第三阶段 · Agent 核心能力

07 · 工具调用

让 Agent 从"只会回答"升级到"会调用真实工具",并把工具结果送入聊天事件流。

课时资源

一、学习目标

本课所在阶段:第三阶段 · Agent 核心能力

学完这一课后,你应该能够:

  • 理解为什么 Chat Bot 不应该只会生成文本,还要能调用真实外部能力
  • 看懂 ToolNodebindTools(...)tool.call 事件在这条链路里的位置
  • 明白工具结果是怎样从 LangGraph 流入前端消息列表的
  • 知道这一课是在"有线程记忆"的基础上继续增加"能行动"的能力

二、问题背景

第六课已经让聊天应用开始具备"记住上下文"的能力,但如果你问它"现在几点"或者输入一个数学表达式,它还是可能只返回一段普通文本。

这说明系统还缺少一层关键能力:不是"会不会继续说",而是"会不会真的做事"。

如果没有这一课,应用会停留在一个很有限的状态:

  • 模型可以根据上下文生成更连贯的文本
  • 但它不能可靠地调用外部能力
  • 前端也无法区分"模型在说话"和"模型刚刚调用过工具"

这一课真正要解决的,是先把最小工具调用闭环跑通:模型决定是否调用工具,工具执行后把结果回到聊天流,前端再把它渲染出来。

三、核心概念

这一课最重要的概念是:把聊天工作流从"纯文本回复"升级为"模型判断 -> 工具执行 -> 继续回复"。

当前实现不是规则匹配版假工具,而是已经切进了 LangGraph 的真实工具调用链路:

  1. model.bindTools(tools) 让模型在本轮对话中看见可用工具

  2. ToolNode 专门负责执行被模型选中的工具

  3. tool.call 事件 把工具调用结果从后端送到前端,而不是只留在 Agent 内部

本课使用的最小工具

  • current_time 获取当前上海时区时间

  • calculator 计算简单数学表达式

这两个工具都很小,但足够把整条链路讲清楚。

这一课相对第 06 课的核心增量

维度第 06 课第 07 课
Agent 能力记住上下文记忆 + 工具调用
LangGraph 图结构只有 chatbot 节点chatbot + tools 节点
模型输出只生成文本可能发起 tool_calls
前端事件流session.start + 文本事件新增 tool.call
消息结构只有文本消息assistant 消息可附带 toolCalls

这一课和参考项目的关系

参考项目同样使用 ToolNode 和工具事件流,但课程版刻意先用两个最小工具把链路讲清楚。这样你会更容易看懂:

  • 模型为什么能自己决定要不要调用工具
  • 工具执行后结果怎样回到聊天流
  • 前端为什么需要知道"刚刚调用过什么工具"

四、整体流程

五、运行过程

1. 模型先看见可用工具

app/agent/chatbot.ts 里,模型不是直接 invoke(state.messages),而是:

const response = await model.bindTools(tools).invoke(state.messages);

这一行意味着:模型本轮不仅能生成文本,还能返回结构化的 tool_calls

2. LangGraph 用条件路由决定是否去执行工具

shouldContinue 会检查最近一条 AI 消息里有没有 tool_calls

  • 有:走到 tools
  • 没有:直接结束

这就是"模型先判断,要不要调用工具"的真正落点。

3. ToolNode 执行工具,不是聊天层手写调用

工具真正执行时,不是在 service 层写 if/else 去调用,而是交给 LangGraph 的 ToolNode(tools)。这能让工具调用继续留在工作流层,后面扩展更多工具时结构不会塌掉。

4. 后端把工具结果送回前端

streamChat(...) 在监听 app.streamEvents(...) 时,不只关心 on_chat_model_stream,还会处理:

  • on_chat_model_end 记录模型刚刚准备调用哪些工具

  • on_tool_end 拿到工具真正执行后的结果,并产出 { type: 'tool', toolCall }

5. 前端把工具记录挂到当前 assistant 消息上

page.tsx 里通过 activeAssistantId 知道当前这轮回复属于哪条 assistant 消息,然后在 tool.call 到来时调用:

setMessages((current) => attachToolCall(current, activeAssistantId, payload as ToolCallRecord));

这样工具结果不会散落成独立消息,而是成为当前回复的补充信息。

六、关键代码解析

app/agent/chatbot.ts - 引入 ToolNode 和条件路由

本课最关键的文件。这里引入了 ToolNodeshouldContinue 条件路由,以及工具事件的提取逻辑。

关键代码:

const workflow = new StateGraph(MessagesAnnotation)
  .addNode('chatbot', async (state) => {
    const model = createModel();
    const response = await model.bindTools(tools).invoke(state.messages);
    return { messages: [response] };
  })
  .addNode('tools', new ToolNode(tools))

代码解析:这里最关键的点是:

  1. 模型不是被动接收工具结果,而是自己决定要不要调用工具
  2. 工具执行被放在独立节点里,而不是塞进 chat.service.ts
  3. 这已经是一个真正的 Agent 工作流,而不是"正则识别后手动调用函数"

app/agent/chatbot.ts - 缓存待完成的工具调用

关键代码:

const pendingToolCalls = new Map<string, ToolCallRecord>();

代码解析:后面在 on_chat_model_end 里先记录工具调用元信息,在 on_tool_end 里再补上真正输出。这么做的原因是:模型发起工具调用和工具执行完成是两个事件,必须先把它们关联起来,前端才能拿到一条完整的工具调用记录。


app/agent/tools/current-time.ts - 定义时间工具

定义时间工具,展示一个真实工具最小需要哪些内容:名字、描述、参数 schema 和执行函数。


app/agent/tools/calculator.ts - 定义计算器工具

定义计算器工具,让你看到"文本提问 -> 结构化参数 -> 真实执行结果"的完整样子。


app/services/chat.service.ts - 把工具结果转成 SSE 事件

负责把 Agent 产生的工具事件转成 tool.call SSE 事件,继续送到前端。

关键代码:

if (item.type === 'tool') {
  latestToolCall = item.toolCall;
  if (item.toolCall) {
    yield { event: 'tool.call', data: item.toolCall };
  }
  continue;
}

代码解析:如果不把工具结果单独作为事件发给前端,前端就只能看到"最终回复文本",却不知道中间到底调用过什么工具。后面做更复杂的工具 UI、审计、调试时,这会是很大的缺口。


app/page.tsx - 把工具调用附着到 assistant 消息

新增 attachToolCall,把工具调用记录附着到当前 assistant 消息上。

关键代码:

setMessages((current) => attachToolCall(current, activeAssistantId, payload as ToolCallRecord));

代码解析:这样工具结果不会散落成独立消息,而是成为当前回复的补充信息。


app/types/chat.ts - 定义工具调用数据结构

新增 ToolCallRecord,定义前后端共用的工具调用数据结构。

七、常见问题

为什么这一课不直接上统一工具配置?

因为你得先确认"真实工具调用本身能不能跑通"。如果连 ToolNodetool.call 和前端附着逻辑都没看懂,直接上统一配置只会把问题叠在一起。

ToolCallRecord 为什么要放到 types/chat.ts

因为它已经不是 Agent 内部私有数据了,而是要跨越后端和前端边界传递的结构。

工具结果为什么还要保留在 assistant 消息旁边?

因为当前主视图仍然是聊天视图。工具调用是这条回复的组成部分,不应该把它和主回复拆成两个互不相关的聊天单元。

八、练习题

  1. 解释 bindTools(...)ToolNodetool.call 三者分别负责什么。
  2. 为什么工具调用不能只停留在 Agent 内部,而要进入前端事件流?
  3. 如果你要新增一个天气工具,最先应该补哪几个文件?
  4. 对比第 06 课和第 07 课,说明"记忆能力"和"行动能力"在系统里的落点有何不同。

九、总结

这一课真正建立的是:Agent 不再只会回答,而是开始具备调用真实外部能力的入口。

只要你已经能说清楚工具怎样被模型选中、怎样被 ToolNode 执行、以及结果怎样通过 tool.call 进入前端,这一课就掌握了。

登录以继续阅读

解锁完整文档、代码示例及更多高级功能。

立即登录

On this page