第 3 卷
复制代码 1--- 装配:一轮要流的几个包 ---
| 2 | llm → sessions → system-prompt → tools → agents → agent-loop |
|---|---|
| 3 | 循环声明它依赖哪些服务(AgentLoop.inject):agents, sessions, llm, tools, systemProm |
| 4 | 一次最多并行几个工具调用:10 |
5 6--- 第 1 轮:模型先要工具,再给答案 ---
7seq 0agent/inbox/spliced 8seq 1turn/start
9seq 2agent/inbox/spliced 10seq 3step/start
11seq 4user/message 12seq 5request/header
13seq 6request/context 14seq 7assistant/chunk
15seq 8assistant/chunk 16seq 9assistant/chunk
17seq 10assistant/chunk 18seq 11assistant/message
19seq 12tool/call 20seq 13tool/result
21seq 14step/end 22seq 15step/start
23seq 16assistant/chunk 24seq 17assistant/chunk
25seq 18assistant/chunk 26seq 19assistant/chunk
27seq 20assistant/message 28seq 21step/end
29seq 22turn/end 30agent/status 变化:running → idle
顺着 seq 把这段读一遍,这一轮的⻣架就出来了:
- agent/inbox/spliced(seq 0、2):队列的入队(seq 0)与取出(seq 2)都是持久
事件,取出发生在下一步 turn/start 之后。第 08 讲说过“进日志即冻结”:谁在什么时候把哪条提示词放进来、又在哪里被取走,账上都有。
的(第 08 讲的投影),所以每次请求前都重算,插话、压缩也都能在发出前生效。
request/header 是这次调用的配置(provider、model,连同渲染好的系统提示词与工具 schema),request/context 记的是这次用的路由与容量(provider、model、
contextWindow),只在它们变化时才记一条。
- assistant/chunk ×4(seq 7–10):模型是流式的,分片先记进日志再组装。所以哪
怕进程中途崩了,已经吐出来的半截也在账上。
- assistant/message(seq 11):流组装完的完整消息。
- tool/call → tool/result(seq 12–13):模型要工具,工具执行,结果记进日志,
成为一条持久事实;模型看到的那一版是从日志投影出来的(第 12 讲讲过:超了阈值,模型看到的就换成预览加指引)。
- step/end → step/start(seq 14–15):第一个 step 结束,因为第一次请求里有工
具调用,循环不停,带着工具结果再发一次请求。
- assistant/message(seq 20)→ step/end(seq 21)→ turn/end(seq 22):
这次模型没有要工具,turn 到此为止,状态回到 idle。
一句话记住这条流程:turn/start 开账 → 每个 step 都是“组装请求 → 流式响应 → 记进日志 → 要不要工具” → 既没有工具、也没有新的 steering(steer() 插进来的输入)才
turn/end。
循环怎么一轮轮跑:跑、停、等
第四个问题:循环怎么一轮轮跑下去?
什么时候跑:被叫起来才跑。循环不会自己往下走,followup 或 steer 才会把它从 idle 叫起来(inject 不叫,它的内容只是排在那儿);叫起来之后它检查队列,有活就开新一
轮,没活就回到 idle。第 2 轮演示的正是这个:
6seq 27user/message 7seq 28assistant/chunk
8seq 29assistant/chunk 9seq 30assistant/chunk
10seq 31assistant/chunk 11seq 32assistant/message
12seq 33step/end 13seq 34turn/end
14agent/status 变化:running → idle 15surface 消息数:4 → 6
两轮的事件形状几乎一样,差别在两处:起点不同,第 2 轮是又一条 followup 排进队列,把它从 idle 重新叫起来;长度也不同,第 1 轮有工具调用,多出一个 step,第 2 轮直接给
答案就收尾了。
队列 agent.inbox 里有两条队,各有名字与分工:
| 队名 | 等待的输入 | 放入的方法 |
|---|---|---|
| next-turn | 等着各开一轮的提示词 | followup()(唤醒) |
| next-step | 等着下一个步骤边界的输入 | steer()(只在闲着时唤醒)、inject()(不唤醒) |
放进去之后,循环在两个时机来取:开新一轮时(轮次边界),一次取走“一条排队的提示词
- 全部 next-step 输入”;同一轮的步骤之间,只取 next-step 输入。所以 inject 进
去的内容不会自己触发运行,它不唤醒 agent,会一直留在 next-step 里,直到下一次 followup 或 steer 唤醒循环时才被一起取走。
什么时候停:循环不设上限。跑完队列只是回到 idle 等着,不算停。那什么时候才真的停?外部想让它停,只有一条路:agent.cancel(cause),主动叫停,中止正在跑的轮次、
连排着的输入一起清掉;注意,whenIdle() 完全不干预,只等它自己把手里的活干完、闲