跳转到内容

双模式 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 中。但这会增加策略间上下文传递的复杂度。

参考来源