上期我们通过 streamSimple 让 LLM 生成了一次完整的回复。但 agent 框架需要更精细的控制:边接收 token 边更新 UI,同时追踪 AssistantMessage 的构建过程。
本期我们从 pi 内部消费事件流的代码 入手——agent-loop.ts:streamAssistantResponse。
源码位置
| 函数 | 文件 | 行号 |
|---|---|---|
streamAssistantResponse | packages/agent/src/agent-loop.ts | 275-368 |
| 事件消费循环 | 同上 | 313-357 |
AssistantMessageEvent 类型 | packages/ai/src/types.ts | 358-370 |
核心函数
async function streamAssistantResponse(
context: AgentContext,
config: AgentLoopConfig,
signal: AbortSignal | undefined,
emit: AgentEventSink,
streamFn?: StreamFn,
): Promise<AssistantMessage> {
// 1. 消息转换(AgentMessage[] → Message[])
const llmMessages = await config.convertToLlm(messages);
// 2. 调用 streamSimple 发起流式请求
const response = await streamFunction(config.model, llmContext, { ...config, apiKey: resolvedApiKey, signal });
// 3. 消费事件流
let partialMessage: AssistantMessage | null = null;
let addedPartial = false;
for await (const event of response) {
switch (event.type) {
case "start":
partialMessage = event.partial;
context.messages.push(partialMessage);
addedPartial = true;
await emit({ type: "message_start", message: { ...partialMessage } });
break;
case "text_start":
case "text_delta":
case "text_end":
case "thinking_start":
case "thinking_delta":
case "thinking_end":
case "toolcall_start":
case "toolcall_delta":
case "toolcall_end":
if (partialMessage) {
partialMessage = event.partial;
context.messages[context.messages.length - 1] = partialMessage;
await emit({ type: "message_update", message: { ...partialMessage }, assistantMessageEvent: event });
}
break;
case "done":
case "error": {
const finalMessage = await response.result();
if (addedPartial) {
context.messages[context.messages.length - 1] = finalMessage;
} else {
context.messages.push(finalMessage);
}
if (!addedPartial) {
await emit({ type: "message_start", message: { ...finalMessage } });
}
await emit({ type: "message_end", message: finalMessage });
return finalMessage;
}
}
}
// 循环结束但未收到 done/error(防御性路径)
const finalMessage = await response.result();
...
}
逐段拆解
1. 消息转换(第 288-296 行)
convertToLlm 将 agent 内部的消息格式(AgentMessage[])转为 LLM 兼容格式(Message[])。这是 agent 与 LLM 之间的桥梁:agent 内部可能持有自定义消息(如通知、状态更新),但在发给 LLM 时需要过滤或转换。
streamSimple 的第二个参数接收的是 LLM 层的 Context,包含 systemPrompt | messages | tools,这正是 convertToLlm 产出的格式。
2. 回调函数 emit(第 25 行)
AgentEventSink 是 agent 向外通知的通道:
type AgentEventSink = (event: AgentEvent) => Promise<void> | void;
agent 的所有 UI 绑定都是通过 subscribe() 挂载到 emit 上的。streamAssistantResponse 在其内部接收流式事件,转换成高层级的 Agent 事件(message_start / message_update / message_end),然后通过 emit 向外广播。
3. partialMessage + addedPartial 双变量(第 310-311 行)
这是 streamAssistantResponse 最精妙的设计:
-
partialMessage:指向当前正在累积的AssistantMessage对象。事件流中的每个事件(text_delta、toolcall_delta)都携带了一个partial字段——它是同一个对象的原地变异。所以partialMessage = event.partial每次只是拿到最新状态的引用。 -
addedPartial:标记start事件是否已触发。某些 provider 可能跳过start直接发text_delta。addedPartial让代码在done时知道:消息已经在context.messages中(只是原地更新),还是需要首次 push。
4. 事件消费循环(第 313-357 行)
start → text_delta★ → (toolcall_start → toolcall_delta★ → toolcall_end)★ → done
| 事件 | partial 中的内容 | emit AgentEvent |
|---|---|---|
start | 空壳 AssistantMessage(含 model/provider 元数据) | message_start |
text_delta | 已累积文本的 AssistantMessage | message_update |
toolcall_delta | 已累积 arguments 的 AssistantMessage | message_update |
done | 完整 final 消息(含 usage/stopReason) | message_end |
error | 含 errorMessage 的 final 消息 | message_end |
text_start / text_end / toolcall_start / toolcall_end / thinking_* 等事件也都走 message_update 分支——它们都携带 partial 字段,且逻辑相同。
5. done/error 的双路径处理(第 342-355 行)
case "done":
case "error": {
const finalMessage = await response.result(); // 从 stream 获取最终消息
if (addedPartial) {
context.messages[context.messages.length - 1] = finalMessage; // 覆盖已有
} else {
context.messages.push(finalMessage); // 首次推入
}
if (!addedPartial) {
await emit({ type: "message_start", message: { ...finalMessage } }); // 补发 start
}
await emit({ type: "message_end", message: finalMessage });
return finalMessage;
}
response.result() 返回最终的 AssistantMessage 对象。当 addedPartial=false(无 start 事件)时,需要在 done 时补发 message_start,保证 UI 收到完整的消息生命周期:start → update* → end。
6. 防御性路径(第 359-368 行)
for await 循环结束后未进入 done/error 分支的情况——极少数 provider 可能不发送终止事件。代码复用 addedPartial 逻辑做兜底处理。