i下面深入讲一下 AIMessage 和 ToolMessage 的区别。

本质区别:一个是”说话人”,一个是”回声” 用一个类比:如果把整个 agent 循环看作一场电话会议,HumanMessage 是用户发言,AIMessage 是模型的发言(可能是纯文本回答,也可能是”我需要查一下资料,请帮我调用 xxx 工具”),ToolMessage 则是别人(工具)汇报调用结果后念给模型听的稿子——它不是模型自己说的话,而是外部世界对模型请求的回执。

字段结构对比 AIMessage ToolMessage 代表谁在”说话” 模型(LLM) 工具执行结果 核心字段 content(文本或多模态列表)+ tool_calls(列表) content(结果)+ tool_call_id(配对用) 触发时机 每次模型推理(invoke)之后产生 工具执行完毕后,由 agent 框架(不是模型)构造 关联关系 自己不知道谁会回复它 必须通过 tool_call_id 精确指向是回答哪一个 tool_calls[i][“id”] 从 context7 拉到的真实 Anthropic 例子能看得很清楚:

AIMessage { “content”: [ {“type”: “text”, “text”: “Here is the calculation…”}, {“type”: “tool_use”, “id”: “toolu_01SQ…”, “name”: “calculator”, “input”: {…}} ], “tool_calls”: [ {“name”: “calculator”, “args”: {…}, “id”: “toolu_01SQ…”, “type”: “tool_call”} ], } ★ Insight ─────────────────────────────────────

content 和 tool_calls 是”原始格式”与”标准化格式”的关系:注意上面例子里,content 里也有一份 tool_use 块,tool_calls 里又重复了一份几乎一样的信息。这不是冗余 bug,而是 LangChain 的适配器模式——content 保留了 Anthropic 原生 API 返回的格式(方便调试、方便看 raw response),而 tool_calls 是 LangChain 抽出来的跨厂商统一接口(不管你用 OpenAI 的 function_call 还是 Anthropic 的 tool_use,上层代码都读 .tool_calls)。这就是为什么 agent.py:69 里判断 if msg.tool_calls: 而不是去解析 content 里的结构——这样换模型厂商代码不用改。

tool_call_id 是防止”张冠李戴”的关键:如果一次 AIMessage 里同时发起了 3 个并行工具调用(比如同时查天气、查汇率、查新闻),返回的 3 条 ToolMessage 必须靠 tool_call_id 才能对上号,顺序是不保证的。这跟网络编程里的 请求-响应关联 ID(如 HTTP 里的 X-Request-ID,或者 RPC 里的 correlation ID)是同一个设计思想:异步/并发场景下,光靠”顺序”是不可靠的,必须靠显式 ID 配对。

回到你选中的代码:agent.py:66-80 里之所以能”无脑”用 isinstance 区分两种消息,是因为 LangChain 的消息类型系统本身就是按”发言角色”设计的(HumanMessage/AIMessage/ToolMessage/SystemMessage),这跟 OpenAI/Anthropic 原生 API 里的 role 字段(user/assistant/tool)是一一对应的——LangChain 只是把”字符串 role + dict content”包装成了带类型的 Python 类,让你能用 isinstance 而不是 msg[“role”] == “tool” 这种脆弱的字符串比较。 ─────────────────────────────────────────────────

一轮完整的消息序列长什么样

[ HumanMessage(“旧金山天气怎么样?”), AIMessage(content=[…], tool_calls=[{“name”: “get_weather”, “args”: {…}, “id”: “call_123”}]), # 模型:我要调用工具 ToolMessage(content=”Sunny, 72°F”, tool_call_id=”call_123”), # 工具:这是结果 AIMessage(content=”旧金山今天晴天,72°F”), # 模型:给用户的最终回答(tool_calls 为空) ] 这正好对应你选中代码里的三段逻辑:有 tool_calls 的 AIMessage(第2条)→ ToolMessage(第3条)→ 无 tool_calls、有 content 的 AIMessage(第4条,触发 “LLM writing final answer…”)。一轮”问答”背后可能是好几条消息的往返,这也是为什么需要 agent.py:66 那个 seen 游标——每次 ask() 调用可能新增不止一条消息。