从零开始拆解Pi系列——(2)消费事件流

0 阅读3分钟

上期我们通过 streamSimple 让 LLM 生成了一次完整的回复。但 agent 框架需要更精细的控制:边接收 token 边更新 UI,同时追踪 AssistantMessage 的构建过程。

本期我们从 pi 内部消费事件流的代码 入手——agent-loop.ts:streamAssistantResponse

源码位置

函数文件行号
streamAssistantResponsepackages/agent/src/agent-loop.ts275-368
事件消费循环同上313-357
AssistantMessageEvent 类型packages/ai/src/types.ts358-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_deltatoolcall_delta)都携带了一个 partial 字段——它是同一个对象的原地变异。所以 partialMessage = event.partial 每次只是拿到最新状态的引用。

  • addedPartial:标记 start 事件是否已触发。某些 provider 可能跳过 start 直接发 text_deltaaddedPartial 让代码在 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已累积文本的 AssistantMessagemessage_update
toolcall_delta已累积 arguments 的 AssistantMessagemessage_update
done完整 final 消息(含 usage/stopReason)message_end
errorerrorMessage 的 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 逻辑做兜底处理。