跳到主要内容

03 · agent 节点:一次模型调用的完整链路

agent 是整张图里唯一会调 LLM 的节点,也是工程量最大的一支。本章端到端追一次 AgentNodeExecutor.execute,看输入怎么变成输出。

1. 它的输入和职责

agent 节点的配置是 AgentConfig:provider(openai/gemini)、name(模型名)、role(系统提示)、tooling(可用工具)、可选 thinking/memories/skills/retry。在 ChatDev_v1.yaml 里,「程序员」「复审员」「测试工程师」都是 agent 节点,区别只在 role 提示词和挂的工具。

它的职责:把上游灌进来的多条 Message,拼成一次(或多次)模型对话,产出一条助手消息

2. 主链路:execute 的七步

AgentNodeExecutor.execute(runtime/node/executor/agent_executor.py:43-177)的骨架:

1. 取 provider(openai/gemini) + 建 client executor:63-78
2. 组装对话:
input_mode=prompt → system(role) + user(拼接文本) :80-81,179-202
input_mode=messages → system + 原始多条 Message :82-83,204-222
3. 可选 pre-generation thinking(改写最后一条 user) :95-104
4. 可选 memory 检索(把召回内容拼进对话) :106-112
5. 调 provider 拿首个响应 :114-123
6. 若响应里有 tool_calls → 进工具循环(见 §3) :125-136
7. 可选 post-generation thinking + 写回 memory → 返回 :144-165

两种输入模式(AgentInputMode,entity/enums.py:82-86) 是个重要分叉:

  • prompt 模式:把所有上游输入压成一段文本塞进一条 user 消息(_inputs_to_text_prepare_prompt_messages)。简单,适合「只要最终文字」的场景。
  • messages 模式:保留每条上游 Message 的角色与结构(_prepare_message_conversation),适合要保留多轮对话/多模态附件的场景。

3. 工具调用循环:调用 → 回灌 → 再问

它要解决的小问题。 模型说「我要调 save_file」时,平台得真去执行这个工具,把结果塞回对话,再问模型下一步——直到模型不再要工具、给出最终答复。

核心循环在 _handle_tool_calls(agent_executor.py:558-612):

# 示意,非源码 —— 复刻工具循环的骨架
while True:
conversation.append(assistant_message) # 把模型的「我要调工具」记进对话
if not assistant_message.tool_calls:
return finalize(assistant_message) # 没有工具调用 → 收工
if iteration >= loop_limit: # 默认 50,见 _get_tool_loop_limit:1061
return finalize(...) # 防止无限调用
tool_msgs, events = execute_tool_batch(...) # 真去执行这批工具
conversation.extend(tool_msgs) # 工具结果回灌对话
assistant_message = invoke_provider(...).message # 带着结果再问模型

一批工具的实际执行在 _execute_tool_batch(agent_executor.py:614-848)。每个工具调用:

  1. 按名字在 tool_specs 里找到对应配置(spec_map),解析 JSON 参数。
  2. 三类分支:① skill 内部工具(activate_skill/read_skill_file)走 _execute_skill_tool;② 普通工具走 tool_manager.execute_tool(agent_executor.py:786-794,注意是 asyncio.run 同步包异步);③ 找不到配置/被 active skill 禁用 → 回一条错误 Message。
  3. 每次调用都 record_tool_call 记 BEFORE/AFTER,异常被吞成一条 TOOL 错误消息(不中断整轮)。

坑/细节: 工具结果可能是纯文本、dict、AttachmentRecord(文件)、甚至嵌套 MessageBlock,_build_tool_message(agent_executor.py:963-999)负责把它们统一成一条 role=TOOL 的消息。模型产出的图片/文件附件会被 _persist_message_attachments(agent_executor.py:1071-1134)落盘到工作区。

4. context_trace:把工具往返「记账」带走

工具循环可能产生很多中间消息(助手要工具、工具回结果、助手再要工具……)。这些不能丢——下一次该节点执行时可能需要。_finalize_tool_trace(agent_executor.py:1004-1017)把整段往返塞进最终消息的 metadata["context_trace"]。执行器在第 02 章提到的「伪自环边」会把它恢复成该节点的输入(GraphExecutor._restore_context_trace,workflow/graph.py:792-816)。这就是 agent 节点能「记得自己上一轮工具对话」的机制。

5. thinking:借鉴自 1.0 的自我反思

thinking 是可选的「想一想」钩子,分 pre-gen / post-gen 两个时机。内置实现 SelfReflectionThinkingManager(runtime/node/agent/thinking/self_reflection.py:10-54)只开 after-gen:把 system/user/assistant 三段对话拼成 prompt,再调一次模型让它「反思并总结要点」,用反思结果替换原输出。其类注释自陈这部分代码「borrowed from ChatDev and adapted」(self_reflection.py:11-13)——也就是把 1.0 里的自我反思思想改写成 2.0 的一个可插拔节点能力。

6. memory:检索增强 + 写回

若节点配了 memories,执行时会:检索(_retrieve_memory,agent_executor.py:522-555)把召回内容拼进对话;结束时写回(_update_memory,agent_executor.py:1253-1286)把本轮输入输出存进记忆库。记忆库类型多样(simple/file/blackboard/embedding/mem0),都在 runtime/node/agent/memory/,通过 MemoryFactory 创建(workflow/graph.py:167-169)。

7. 容错:重试策略

模型调用包了 tenacity 重试(_execute_with_retry,agent_executor.py:394-432):指数退避、只对可重试异常(限流/超时/连接类,见 agent.pyDEFAULT_RETRYABLE_* 常量)重试。即使 YAML 没配 retry,也会兜底给一个默认策略(_resolve_retry_policy,agent_executor.py:434-449)。整个 execute 还包了一层 try/except,任何异常都降级成一条「Error calling model」消息返回(agent_executor.py:167-175),保证单节点失败不炸整图。


下一章: 把视角拉回边和上下文——context_window 清理、keep_message、keyword 路由、map/tree 扇出这些「精华机制」。