双模式 Agent 架构:Function Calling 与 ReAct
学习目标
- 理解 Dify 如何通过共享抽象层统一 Function Calling 与 ReAct 两种 Agent 模式
- 掌握双 Runner 架构的设计决策:共享基础设施 vs 独立执行逻辑
- 学会分析 Agent 迭代循环中的停止条件、用量统计与异常处理
前置知识
本章涉及 Agent 核心循环的通用原理,建议先阅读:
下文假设你已理解 Turn 级别循环的概念,直接聚焦 Dify 的具体实现。
项目实践
共享抽象层:BaseAgentRunner
Dify 的 Agent 系统建立在 BaseAgentRunner 之上,该类继承了 AppRunner 并封装了所有 Agent 运行所需的共享基础设施:
# 伪代码,展示 BaseAgentRunner 的职责范围class BaseAgentRunner(AppRunner): # 共享状态 tenant_id: str # 租户隔离 model_instance: ModelInstance # 模型调用实例 memory: TokenBufferMemory # 对话记忆 queue_manager: AppQueueManager # 事件队列管理器
# 共享能力 def organize_prompt_messages() # 组织提示词 def recalc_llm_max_tokens() # 动态计算最大 Token 数 def create_agent_thought() # 创建 Agent 思考记录 def _init_prompt_tools() # 将工具转换为模型可用的格式BaseAgentRunner 承担了以下通用职责:
- 历史管理:通过
TokenBufferMemory管理对话上下文,控制 Token 预算 - 工具初始化:将 Dify 内部的工具实体转换为模型运行时格式(
PromptMessageTool) - 队列发布:通过
AppQueueManager向流式前端推送 Agent 思考事件 - Token 预算:动态调整 LLM 的
max_tokens参数,避免超出模型上下文窗口
Function Calling Runner:直接工具调用
FunctionCallAgentRunner 实现了基于 Function Calling 的 Agent 循环:
# 伪代码,展示核心循环class FunctionCallAgentRunner(BaseAgentRunner): def run(): while function_call_state and iteration_step <= max_iteration_steps: function_call_state = False
# 最后一步移除工具,强制模型生成最终回复 if iteration_step == max_iteration_steps: prompt_messages_tools = []
# 调用 LLM,传入工具定义 chunks = model_instance.invoke_llm( prompt_messages=prompt_messages, tools=prompt_messages_tools, stream=stream, )
# 解析流式输出,检测 tool_call for chunk in chunks: if chunk.delta.tool_calls: function_call_state = True # 执行工具,获取观察结果 tool_responses = tool_engine.invoke_tools(...) # 将工具结果追加到 prompt_messages prompt_messages.append(ToolPromptMessage(...))核心特征:
- 依赖模型原生能力:要求模型支持 OpenAI 兼容的 Function Calling 协议
- 结构化输出:工具调用以结构化 JSON 返回,无需解析自然语言
- 最后一步强制收敛:达到最大迭代步时清空工具列表,避免无限循环
ReAct Runner:Chain of Thought 推理
CotAgentRunner 实现了基于 CoT(Chain of Thought)的 ReAct 模式:
# 伪代码,展示与 FC Runner 的差异点class CotAgentRunner(BaseAgentRunner, ABC): def run(): # 配置停止词:确保 "Observation" 在停止列表中 if "Observation" not in model_conf.stop: model_conf.stop.append("Observation")
while function_call_state and iteration_step <= max_iteration_steps: # 调用 LLM(不传工具列表) response = model_instance.invoke_llm( prompt_messages=prompt_messages, # 注意:此处没有 tools 参数 )
# 解析 CoT 输出:Thought → Action → Action Input actions = CotAgentOutputParser.parse(response)
if actions: # 执行 Action,获取 Observation observation = tool_engine.invoke_tool(actions) # 将 Observation 追加到提示词中 prompt_messages.append(AssistantPromptMessage(observation)) else: # 无 Action → 模型已生成最终答案 function_call_state = False核心特征:
- 自然语言推理:通过 prompt 模板引导模型输出
Thought → Action → Observation格式 - 停止词控制:将
"Observation"加入停止列表,防止模型自行生成观察结果 - 输出解析器:
CotAgentOutputParser负责从自然语言中提取 Action 和 Action Input
迭代控制与用量管理
两种 Runner 共享以下迭代控制逻辑:
| 要素 | 实现方式 |
|---|---|
| 最大迭代步 | max_iteration = min(config.agent.max_iteration, 99) + 1 |
| 用量累加 | 每次 LLM 调用返回 LLMUsage,累加 prompt/completion tokens 和费用 |
| 异常处理 | 捕获 AgentMaxIterationError,通过 GraphRunFailedEvent 报告 |
| 流式推送 | 通过 AppQueueManager 推送 QueueAgentThoughtEvent 到前端 |
问题与规避
陷阱 1:Function Calling 的模型兼容性
Function Calling Runner 要求模型原生支持工具调用协议。对于不支持的模型(如早期的开源模型),需要回退到 ReAct 模式。
Dify 的规避策略:在应用配置层指定 Agent 模式(agent_chat vs chat),根据所选模型的能力自动选择合适的 Runner。
陷阱 2:ReAct 模式下 Observation 泄露
如果模型的停止词配置不正确,模型可能自行生成 Observation 内容,导致推理链断裂。
Dify 的规避策略:
if "Observation" not in model_conf.stop: if model_conf.provider not in _ignore_observation_providers: model_conf.stop.append("Observation")同时对特定 Provider(如文心)跳过此检查,因为其行为模式不同。
陷阱 3:Token 预算超支
随着迭代次数增加,prompt_messages 不断增长,可能超出模型的上下文窗口。
Dify 的规避策略:每轮迭代前调用 recalc_llm_max_tokens(),根据当前 prompt 长度动态调整 max_tokens。当 prompt 过长时,减少生成的 token 上限。
设计取舍
双 Runner vs 统一 Runner
Dify 选择了双 Runner 架构而非在单个 Runner 内部根据模式切换逻辑:
优势:
- 每种模式的执行逻辑完全独立,代码清晰
- 各自可以针对协议特点做深度优化(如 FC 的结构化解析、ReAct 的 prompt 模板定制)
- 异常隔离:一种模式的 bug 不影响另一种模式
代价:
- 共享逻辑必须在
BaseAgentRunner中抽象,增加了基类的复杂度 - 新增第三种模式时,需要重新评估哪些逻辑可以共享
替代方案:使用策略模式,将 FC 和 ReAct 作为不同的策略注入到统一的 Agent Runner 中。但这会增加策略间上下文传递的复杂度。
参考来源
- ReAct: Synergizing Reasoning and Acting in Language Models
- OpenAI Function Calling
- Dify Agent 模块源码:
api/core/agent/