1. 一个很常见的使用场景
前面一篇把 Tool Calling 拆开看了一遍,原理已经清楚了。
真正写应用时,我们很少停在「模型先提请求,我再手动执行工具」这一步,更常见的是直接把几个工具交给一个 Agent,让它自己决定这一轮怎么走。
AI 伴侣里很容易遇到这样的请求:
1明天上海天气怎么样?如果下雨,明早提醒我带伞。顺便看看我明天有什么安排。
这不是三个独立问题,而是一句话里的三件事:
- 先查天气
- 如果天气结果触发条件,再建提醒
- 再去查一次日程
这种场景正适合单个 Agent。
因为这里还没有出现多个角色分工,也没有复杂审批流。我们只是希望有一个 Agent 能在同一轮里连续用几个工具,把事情做完,然后给用户一个自然的回复。
2. 先看最后要做成什么样
先不急着拆代码,先看最后调用时应该长什么样。
01const result = await agent.invoke({02messages: [03{04role: 'user',05content: '明天上海天气怎么样?如果下雨,明早提醒我带伞。顺便看看我明天有什么安排。',06},07],08})0910console.log(result.messages.at(-1)?.text)
真正对外暴露的入口,最好就这么简单。
调用这一行代码时,我们希望 Agent 能自己完成这些事:
- 先判断这句话里需要哪些工具
- 先查天气
- 如果天气结果满足条件,再去建提醒
- 再查日程
- 最后把结果整理成一段像人说的话
前面手动循环那篇,是为了把过程讲透。
从这一篇开始,就不再让调用方手动维护那条循环了。
3. 先把三个工具准备好
这三个工具本身都不复杂,关键是职责要清楚。
01import { tool } from 'langchain'02import { z } from 'zod'0304// 只负责查天气。05// 正式项目里,这里通常会请求天气 API。06export const getWeather = tool(07async ({ city }) => {08const data: Record<string, string> = {09上海: '明天小雨,17-22 度',10北京: '明天晴,12-25 度',11深圳: '明天多云,26-30 度',12}1314return data[city] ?? `${city}:暂无天气数据`15},16{17name: 'get_weather',18description: '查询某个城市未来的天气情况',19schema: z.object({20city: z.string().describe('要查询天气的城市名'),21}),22},23)2425// 只负责创建提醒。26// 返回对象会比纯字符串更清楚,后面模型也更容易读。27export const createReminder = tool(28async ({ content, time }) => {29return {30ok: true,31message: `提醒已创建:${time} - ${content}`,32}33},34{35name: 'create_reminder',36description: '帮用户创建一个提醒事项',37schema: z.object({38content: z.string().describe('提醒内容'),39time: z.string().describe('提醒时间,例如 明天早上 8 点'),40}),41},42)4344// 只负责查日程。45// 正式项目里,这里通常会查数据库或者日历服务。46export const querySchedule = tool(47async ({ date }) => {48const schedules: Record<string, string> = {49明天: '10:00 产品评审会,14:00 和小李 1v1',50后天: '全天无日程',51}5253return schedules[date] ?? `${date}:没有找到日程`54},55{56name: 'query_schedule',57description: '查询用户某一天的日程安排',58schema: z.object({59date: z.string().describe('要查询的日期,例如 今天、明天、后天'),60}),61},62)
这里最容易写乱的地方,其实不是 tool() 这个 API,而是边界。
如果一个工具里既查天气又建提醒又查日程,模型会更难选,也更难把参数填稳。
把工具拆得明确一点,后面调试会轻松很多。
4. 把它们交给同一个 Agent
到这里,Agent 的代码反而比工具本身还短。
01import { createAgent } from 'langchain'02import { getWeather, createReminder, querySchedule } from './companion-tools'0304const agent = createAgent({05model: 'openai:gpt-4.1-mini',06tools: [getWeather, createReminder, querySchedule],07systemPrompt: `08你是用户的 AI 伴侣。0910当用户的问题涉及天气、提醒和日程时,使用对应工具。11如果一句话里有多件事,就按顺序处理。12如果用户只是在聊天,就直接回复,不要强行调用工具。13`.trim(),14})
这里有一个变化很重要。
前一篇里,我们自己维护:
- 模型返回
tool_calls - 程序执行工具
- 再把工具结果交还给模型
这一篇里,这段循环已经交给 createAgent(...) 了。
所以你在入口上只看到一次 agent.invoke(...),但 Agent 内部可能已经跑了不止一次工具调用。
5. 一轮请求里,Agent 可能会连续做几件事
先看一段完整调用:
01import { createAgent } from 'langchain'02import { getWeather, createReminder, querySchedule } from './companion-tools'0304const agent = createAgent({05model: 'openai:gpt-4.1-mini',06tools: [getWeather, createReminder, querySchedule],07systemPrompt: `08你是用户的 AI 伴侣。09当用户的请求涉及天气、提醒和日程时,使用对应工具。10如果一句话里有多件事,就按顺序处理。11`.trim(),12})1314const result = await agent.invoke({15messages: [16{17role: 'user',18content: '明天上海天气怎么样?如果下雨,明早提醒我带伞。顺便看看我明天有什么安排。',19},20],21})2223// 最后一条消息就是这一轮最终回复。24// 前面的工具调用和中间结果,Agent 已经替我们处理好了。25console.log(result.messages.at(-1)?.text)
这段代码对外只有一个入口,但 Agent 在内部很可能会走成这样:
11. 先调用 get_weather({ city: '上海' })22. 读到天气结果是“小雨”33. 再调用 create_reminder({ content: '带伞', time: '明天早上' })44. 再调用 query_schedule({ date: '明天' })55. 最后把天气、提醒、日程合并成一段回复
也就是说,单个 Agent 不是「一次请求只能调一个工具」。
只要这句话里确实需要多步动作,它可以在同一轮里连续往下走。
如果你想把这个过程看得更明显一点,可以直接把消息打印出来:
1for (const message of result.messages) {2// 这里通常能看到:3// user 消息4// assistant 发起的工具调用5// tool 返回结果6// assistant 最终回复7console.log(message.getType(), message.text ?? message.content)8}
这一段很适合调试。
当你怀疑 Agent 为什么没调工具、或者调错了工具,先看 result.messages 往往比先改 Prompt 更有用。
6. 什么时候一个 Agent 还够用
写到这里,单个 Agent 已经能做不少事了。
像下面这种情况,单个 Agent 往往就够:
用户一句话里有几步动作,但都属于同一个角色在帮他办事。
比如查天气、建提醒、查日程、查知识库、发一条总结,这些都还是同一个生活助理的职责。
这时候继续拆成多个 Agent,收益不一定大,反而会先把结构变复杂。
单个 Agent 先把这些工具接稳,通常是更顺的做法。
真正开始需要再往下拆,一般是因为下面这些情况开始出现了:
- 工具越来越多,描述开始互相打架
- 一轮请求里不仅是多步动作,还开始出现不同角色分工
- 某些步骤需要长期状态、审批、回退或人工介入
那时就不是这一篇要解决的问题了。
7. 写这种 Agent 时,最容易卡住的地方
最常见的卡点通常不是 createAgent(...) 本身,而是下面这几种情况。
工具的描述太短,模型不知道什么时候该用它。
比如 description 只写「查询信息」,那和不写差不多。描述里最好直接点出场景和用途。
工具边界太宽,一个工具里包了太多动作。
模型要先猜 action,再猜 payload,最后还要靠你在函数里二次分发,这一层很容易出问题。
一句话里有多件事,但 systemPrompt 没告诉 Agent 可以按顺序处理。
这时模型有时会只做第一件,后面的动作直接漏掉。
还有一种很常见:工具其实已经调了,但你只盯着最终回复,没看中间消息。
这时最简单的办法不是猜,而是把 result.messages 打出来看。
单个 Agent 调多个工具,说到底没有多神秘。
前面几篇准备的东西,走到这里刚好接上:消息、Prompt、LCEL、检索、Tool,最后都汇成这一个入口。