数据截至 (上游 commit 136cd875b154)
Agent 主循环:流式工具调用
30 秒导读: 用户在 nao 聊天框里问一句"上个月哪个渠道转化最高?",后端并不是"问一次答一次"。 它把这次提问装配成一个会自己调工具的 agent:模型先想,调
execute_sql查库,拿到结果再想, 也许再画个图,最后总结——这一整串"生成→调工具→再生成"的循环,就是本章的主角。我们只讲 这个循环怎么被驱动起来、怎么边跑边流式吐字、怎么在结束时落库;不讲每个工具内部怎么实现(见 03-tools.md),也不讲系统提示的文本怎么拼(见 04-system-prompt.md)。
本章的全部戏都发生在一个文件里:apps/backend/src/services/agent.ts。读完你能讲清楚:一次 HTTP
请求进来后,是谁解析了模型、谁决定了这个循环"什么时候停"、模型说的话是怎么变成前端能渲染的流、
以及为什么每一步之前都要"剪枝 + 打缓存标记"。
1. 先建立直觉:什么是"Agent 主循环"
1.1 一次问答不是一次调用
朴素的 LLM 调用是"一发一收":给提示,收一段文字。但数据分析要动手——查数据库、跑 Python、
画图表。模型不能自己查库,它只能"说":我想调 execute_sql,参数是这段 SQL。于是后端要:
- 把模型这句"我想调工具"接住;
- 真的去执行那个工具(连库、跑查询);
- 把结果塞回对话,再让模型继续;
- 重复,直到模型说"我讲完了"。
这个"生成一步 → 若要调工具就执行 → 把结果喂回去 → 再生成"的往复,就是 agent loop(智能体主循环)。 一次用户提问,可能在后端滚动好几步(step),才有最终答案。
1.2 nao 不自己写循环,它"外包"给 SDK
关键设计:nao 不手写这个 while 循环。它用 Vercel AI SDK(ai 包,v6)提供的
ToolLoopAgent——一个已经把"多步工具循环"封装好的类。nao 的活是装配:选好模型、备好工具、
定好"何时停",把这些交给 ToolLoopAgent,然后调它的 stream() 让循环跑起来。
一句话类比:
ToolLoopAgent是一台"自动驾驶的对话引擎",nao 是"上车前的配置员"—— 加什么油(模型)、带什么工具、走到哪儿算到站(停止条件),都由 nao 在发车前设好;开起来之后的 每一步换挡由引擎自己完成。
1.3 两个人格:AgentService 和 AgentManager
agent.ts 里有两个类,别名一下就不会混:
| 类 | 角色 | 一句话职责 | 位置 |
|---|---|---|---|
AgentService | 工厂 + 登记处 | 解析模型/工具/上下文/停止条件,造出一个 agent,并按 chatId 登记 | services/agent.ts:203 |
AgentManager | 一次会话的引擎壳 | 持有 ToolLoopAgent,提供 stream() / generate(),负责装配消息、流式、落库 | services/agent.ts:362 |
AgentService 是单例(文件末尾 export const agentService = new AgentService(),services/agent.ts:1080);
每个活跃 chat 对应一个 AgentManager,存在 AgentService._agents 这张 Map<chatId, AgentManager> 里。