数据截至 (上游 commit 59a71b235dad)
pi-agent-core:agent 循环与工具调用
30 秒导读:
@earendil-works/agent这个包,是 Pi 项目名所指的那颗「心脏」——一个与 provider 无关的 agent 运行时循环。你喂给它一段 prompt、一组工具、一个 model,它就反复地「让模型说话 → 执行模型点名的工具 → 把结果喂回去」,直到模型不再要工具为止;期间还允许你中途插话(steering)和排队追 问(follow-up)。 本章只讲这颗心脏本身:纯循环 + 工具编排 + 事件流。会话/压缩/系统提示见 03 章, 具体工具实现见 04 章,底层统一 LLM 层见 01 章。
1. 这是什么(零基础也能懂)
一句话定义: 一段「让 LLM 反复调用工具直到完成任务」的循环代码,加上一个有状态的门面类 Agent。
它解决什么问题。 单次调用 LLM 只能得到一段文字或一个「我想调用 read_file」的请求——它自己没有手,
执行不了。要让模型真正「干活」,你得写一个循环:把模型的工具请求真正执行掉,再把结果塞回对话,让它接着想
下一步。这段循环 90% 的项目都在重复造轮子,而且容易写错(并发、中止、错误处理、插话)。agent 包把这段
循环抽成一个可复用、provider 无关、事件驱动的库。
给谁用。 想自己搭一个编码 agent / 自动化 agent 的开发者。Pi 的编码 agent(04 章)就是在这颗心脏外面 套了工具集、会话、CLI。
它能做什么:
- 驱动「助手回复 ↔ 工具执行」的多轮循环,直到模型停止要工具。
- 工具批次可串行或并行执行,且可按工具粒度覆盖。
- 支持三种「插话」时机:steering(干活途中插)、follow-up(本要停下 时追问)、以及一批钩子回调。
- 全程发事件流(agent/turn/message/tool 各级生命周期),UI 可实时渲染。
- 与 provider 解耦:真正调模型的那一步是一个可替换的
streamFn(默认走 01 章的streamSimple, 也可换成streamProxy走自己的服务器)。
用起来什么样。 门面类 Agent 让最小用法只有几行(下面是示意,非源码):
// 示意,非源码 —— 展示 Agent 门面最小用法
const agent = new Agent({
initialState: { systemPrompt, model, tools }, // 系统提示 + 模型 + 工具
});
agent.subscribe((event) => render(event)); // 订阅事件流做 UI
await agent.prompt("把 src 下的测试都跑一遍"); // 发一句话,循环自动转起来
// 干活途中想补一句 → agent.steer(...); 想等它停下再问 → agent.followUp(...)
一句话直觉。 把它想成一台传送带:模型放上一个「我要调 X 工具」的零件,循环把零件加工(执行工具)、 再放回传送带让模型接着看;传送带一圈圈转,直到模型说「我不需要更多零件了」。你可以在传送带转的时候往上 丢新零件(steering),也可以等它空转时再补料(follow-up)。
2. 顶层全景(它大概怎么转)
整个包围绕一个数据类型和一个双层循环展开。
2.1 部件一句话职责
| 部件 | 干什么 | 在哪 |
|---|---|---|
Agent 门面类 | 有状态封装:持有 transcript、发事件、管两个插话队列、暴露 prompt/continue/steer/followUp | agent.ts:173 |
runLoop | 无状态的纯循环核心:双层 while,编排一切 | agent-loop.ts:155 |
agentLoop / agentLoopContinue | 两个入口:带新 prompt 起循环 / 从现有 context 续跑 | agent-loop.ts:31,64 |
streamAssistantResponse | 一次「让模型说话」:AgentMessage[]→Message[] 的转换边界就在这 | packages/agent/src/agent-loop.ts:281 |
executeToolCalls | 执行一批工具调用(串行 / 并行) | agent-loop.ts:411 |
EventStream | 通用异步事件流,循环把事件 push 进去、消费者 for await 出来 | packages/ai/src/utils/event-stream.ts:4 |
streamFn(默认 streamSimple,或 streamProxy) | 真正调 provider 的可替换函数 | types.ts:27、proxy.ts:118 |
2.2 数据是怎么流的
关键设计:循环内部一路用 AgentMessage,只有在调模型的前一刻才转成 provider 认识的 Message。
这让 app 能往对话里塞自定义消息(UI 通知、状态卡)而不污染发给模型的内容。
怎么读下图:从上到下是一次 prompt 的主干;右侧标了「谁在这一步插得进话」。
agent.prompt("...")
│
┌─────────▼──────────────────────────────────────────┐
│ runLoop 外层循环(follow-up 层) │
│ ┌──────────────────────────────────────────────┐ │
│ │ 内层循环(工具批次 + steering 层) │ │
│ │ │ │
│ │ ① 注入 pendingMessages ───────◄── steering │ │
│ │ ② streamAssistantResponse │ │
│ │ AgentMessage[] ──convertToLlm──► Message[]│ │
│ │ └──► streamFn ──► provider(01 章) │ │
│ │ ③ 过滤出 toolCall │ │
│ │ ④ executeToolCalls(串行/并行)→ toolResults │ │
│ │ ⑤ turn_end / prepareNextTurn │ │
│ │ ⑥ 还有工具? 或 有 steering? ──是──► 回 ① │ │
│ └──────────────────┬────────────────────────────┘ │
│ 否(本要停) │ │
│ ⑦ getFollowUpMessages ──有──► 设为 pending,回内层 ◄─ follow-up
│ 无 │ │
└─────────────▼───────────────────────────────────────┘
agent_end
主线走一遍(高层): 发 prompt → 进内层循环 → 模型说话 → 若点名工具就执行、结果塞回 → 只要还有工具或有人
steering 就继续转 → 内层空了看有没有 follow-up → 有就再进内层,没有就 agent_end 收尾。
3. 核心数据类型(循环搬运的都是这些)
先认清类型,后面的循环才好懂。全部在 packages/agent/src/types.ts。
3.1 AgentMessage:循环的「货物」
// types.ts:314
export type AgentMessage = Message | CustomAgentMessages[keyof CustomAgentMessages];
AgentMessage = provider 的标准 Message(user / assistant / toolResult)并上 app 自定义消息。app 通过
TypeScript 的声明合并往 CustomAgentMessages 接口里加自己的消息类型(types.ts:305)。这就是「循环内一路
用 AgentMessage」的意义:通知、状态卡都能进 transcript,但发给模型前会被 convertToLlm 过滤掉(见 §5.1)。
3.2 AgentTool / AgentToolResult:工具长什么样
AgentTool(types.ts:371)在 01 章的 Tool(名字 + TypeBox 参数 schema)之上,加了:
| 字段 | 作用 |
|---|---|
label | UI 显示用的人类可读名 |
execute(id, params, signal?, onUpdate?) | 真正干活;失败要 throw,不要把错误编码进 content |
prepareArguments? | schema 校验前对原始 args 的兼容性修正 |
executionMode? | 按工具覆盖串行/并行(见 §6) |
execute 返回 AgentToolResult<T>(types.ts:350):content(回给模型的文字/图像)+ details(给日志/UI
的结构化数据)+ 可选 terminate(提示这批工具跑完就该停,见 §6.3)。onUpdate 回调让长工具流式吐进度。
3.3 AgentContext / AgentLoopConfig:一次运行的两半
AgentContext(types.ts:397):运行的数据——systemPrompt+messages+tools。AgentLoopConfig(types.ts:140):运行的行为——model、那些回调钩子、convertToLlm、toolExecution等。它extends SimpleStreamOptions(温度、maxTokens 等直接透传给 provider)。
3.4 AgentEvent:循环对外广播的一切
AgentEvent(types.ts:413)是一个可辨识联合,按四级生命周期分组:
| 级别 | 事件 |
|---|---|
| agent | agent_start / agent_end(带最终 messages) |
| turn(一次助手回复 + 其工具) | turn_start / turn_end(带 message 和 toolResults) |
| message | message_start / message_update(仅流式助手消息)/ message_end |
| tool | tool_execution_start / tool_execution_update / tool_execution_end |
3.5 三个小枚举
ThinkingLevel(types.ts:289):off/minimal/low/medium/high/xhigh,推理档位。ToolExecutionMode(types.ts:41):"sequential" | "parallel",工具批次执行策略。QueueMode(types.ts:49):"all" | "one-at-a-time",排队消息一次注入几条(见 §7)。
4. 循环结构:双层 while 与 firstTurn / pendingMessages
这是全章的骨。所有编排都在 runLoop(agent-loop.ts:155)里。