这就是编程:Pi Monorepo 源码深度--解析一个工业级 AI Agent 框架的设计哲学

0 阅读14分钟

当我第一次打开 agent-loop.ts 时,脑子里第一个念头是:这也太简单了吧?418 行,没有繁重的抽象,没有依赖注入容器,没有魔法装饰器。但读完整个 packages/agent 目录之后,我改变了看法——这种"简单"背后藏着极其清晰的工程判断:每一个设计决策都有明确的理由,每一层抽象都恰好到位,不多也不少。

这篇文章是我阅读 Pi Monorepo 源码的记录。它是一个用 TypeScript 写的 AI Agent 基础设施项目,核心目标只有一个:让构建可用于生产的 AI Agent 变得可预测。我会从架构全貌讲到最核心的执行循环,试图还原作者在每个设计节点上的思考脉络。

目录

  1. 项目简介与使用场景
  2. 与主流 Agent 框架的对比
  3. 整体架构设计
  4. 核心模块深度解析
  5. 完整交互流程分析
  6. 设计亮点总结

项目简介与使用场景

Pi Monorepo 是一个以 TypeScript 编写的工业级 AI Agent 基础设施工具集,托管于 github.com/badlogic/pi…。它不是一个单一的框架,而是一组经过精心设计的可组合包:

包名核心职责
@mariozechner/pi-ai统一多 Provider LLM API(OpenAI、Anthropic、Google、Bedrock 等)
@mariozechner/pi-agent-coreAgent 运行时:工具调用、状态管理、事件流
@mariozechner/pi-coding-agent交互式代码生成 CLI
@mariozechner/pi-momSlack Bot,将消息委托给编码 Agent 处理
@mariozechner/pi-tui终端 UI 库(差量渲染)
@mariozechner/pi-web-uiAI 聊天界面 Web 组件
@mariozechner/pi-podsvLLM GPU Pod 部署管理 CLI

典型使用场景

场景一:多模型编码助手 开发者需要构建一个可以自由切换 Claude、GPT-4o、Gemini 的编码助手,同时支持工具调用(读文件、执行命令)。pi-ai 提供统一接口,pi-agent-core 处理工具调用循环,pi-coding-agent 则是这一组合的完整实现。

场景二:企业内网代理 企业不想让客户端直接访问 LLM Provider API Key,需要通过内部网关统一鉴权和路由。proxy.ts 中的 streamProxy 函数提供了开箱即用的 SSE 代理流支持。

场景三:Slack 工作流自动化 pi-mom 包监听 Slack 频道消息,触发编码 Agent 执行任务,完成后回复结果——这是典型的长链 Agent 场景。

场景四:vLLM 私有化部署 pi-pods 管理在 GPU Pod 上的 vLLM 实例生命周期,将自托管模型纳入统一的 pi-ai 接口体系。


与主流 Agent 框架的对比

┌─────────────────────────────────────────────────────────────────────┐
│                        Agent 框架横向对比                             │
├──────────────┬──────────────┬──────────────┬──────────────┬─────────┤
│              │   LangChain  │  AutoGen/AG2 │  CrewAI      │  pi-mono│
├──────────────┼──────────────┼──────────────┼──────────────┼─────────┤
│ 语言         │ Python/JS    │ Python       │ Python       │TypeScript│
│ Provider抽象 │ ✓ 丰富       │ △ 有限       │ △ 有限       │ ✓ 完整  │
│ 流式输出     │ △ 部分支持   │ ✗            │ ✗            │ ✓ 原生  │
│ 思维链支持   │ △            │ △            │ △            │ ✓ 内置  │
│ 工具调用中断 │ ✗            │ △            │ ✗            │ ✓ 原生  │
│ 类型安全     │ △            │ △            │ △            │ ✓ 严格  │
│ 代理模式     │ △            │ ✗            │ ✗            │ ✓ 内置  │
│ 包体积       │ 极重         │ 重           │ 中           │ 轻量    │
└──────────────┴──────────────┴──────────────┴──────────────┴─────────┘

Pi Mono 相对优势

  1. 原生流式 + 事件驱动:从最底层的 Provider 响应到最上层的 UI 更新,整个调用链路全程流式,UI 无需轮询。
  2. Steering / FollowUp 中断机制:用户可以在 Agent 执行工具调用期间注入"方向盘消息",跳过剩余工具调用并立即响应——这在 LangChain 等框架中需要大量定制代码。
  3. TypeBox 参数验证:工具参数使用 @sinclair/typebox 进行运行时类型验证,错误在执行前被捕获。
  4. 无 any 类型设计:整个代码库几乎没有 any,从 Provider 响应到工具结果全部端对端类型安全。
  5. 轻量内核agent-loop.ts 核心逻辑仅 418 行,无隐式依赖魔法。

相对局限

  • 生态系统相对年轻,社区工具和预构建工具集比 LangChain 少
  • 无内置的 RAG / 向量检索管道(需自行集成)
  • 多 Agent 编排(如 AutoGen 的对话图)需自行实现

整体架构设计

┌─────────────────────────────────────────────────────────────────────┐
│                         pi-mono 分层架构                              │
│                                                                     │
│  ┌─────────────────────────────────────────────────────────────┐   │
│  │                     应用层 (Application)                      │   │
│  │   pi-coding-agent CLI    pi-mom Slack Bot    Web UI / TUI    │   │
│  └──────────────────────────────┬──────────────────────────────┘   │
│                                 │ uses                              │
│  ┌──────────────────────────────▼──────────────────────────────┐   │
│  │               pi-agent-core  (Agent Runtime)                 │   │
│  │                                                              │   │
│  │   ┌──────────────┐   ┌──────────────┐   ┌──────────────┐   │   │
│  │   │  Agent class  │   │  agent-loop  │   │    proxy     │   │   │
│  │   │ (状态管理封装) │   │  (执行核心)  │   │  (代理模式)  │   │   │
│  │   └──────┬───────┘   └──────┬───────┘   └──────────────┘   │   │
│  │          └──────────────────┘                               │   │
│  └──────────────────────────────┬──────────────────────────────┘   │
│                                 │ calls                             │
│  ┌──────────────────────────────▼──────────────────────────────┐   │
│  │                     pi-ai  (LLM 抽象层)                       │   │
│  │                                                              │   │
│  │   ┌──────────────────────────────────────────────────────┐  │   │
│  │   │              API Registry  (提供商注册表)              │  │   │
│  │   └──────────────────────────────────────────────────────┘  │   │
│  │                                                              │   │
│  │  ┌──────┐ ┌──────────┐ ┌───────┐ ┌────────┐ ┌──────────┐  │   │
│  │  │OpenAI│ │Anthropic │ │Google │ │Bedrock │ │  Mistral │  │   │
│  │  │      │ │          │ │Vertex │ │(lazy)  │ │  ...etc  │  │   │
│  │  └──────┘ └──────────┘ └───────┘ └────────┘ └──────────┘  │   │
│  └──────────────────────────────────────────────────────────────┘   │
└─────────────────────────────────────────────────────────────────────┘

核心设计原则:分层隔离,向下依赖。应用层只依赖 pi-agent-corepi-agent-core 只依赖 pi-aipi-ai 内部通过注册表模式管理各 Provider,各层职责清晰。


核心模块深度解析

pi-ai:统一 LLM 抽象层

类型系统基础

pi-ai 的核心是一套精心设计的类型体系(packages/ai/src/types.ts):

// 消息类型:User / Assistant / ToolResult 三元组
export type Message = UserMessage | AssistantMessage | ToolResultMessage;

// Assistant 消息内容块支持文本、思维链、工具调用
export interface AssistantMessage {
  role: "assistant";
  content: (TextContent | ThinkingContent | ToolCall)[];
  api: Api;
  provider: Provider;
  model: string;
  usage: Usage;            // 完整的 Token 计量(含 Cache Read/Write)
  stopReason: StopReason;
  timestamp: number;
}

// 工具调用完整描述
export interface ToolCall {
  type: "toolCall";
  id: string;
  name: string;
  arguments: Record<string, any>;
  thoughtSignature?: string; // Google 特有:复用思维上下文
}

AssistantMessageEvent 定义了一条消息从生成到完成的完整生命周期事件序列:

start → text_start → text_delta(×N) → text_end
      → thinking_start → thinking_delta(×N) → thinking_end
      → toolcall_start → toolcall_delta(×N) → toolcall_end
      → done | error

Provider 注册表模式

┌─────────────────────────────────────────────────────────┐
│                    API Registry 设计                      │
│                                                         │
│  registerApiProvider({ api, stream, streamSimple })     │
│           │                                             │
│           ▼                                             │
│  apiProviderRegistry: Map<string, ApiProvider>          │
│                                                         │
│  ┌────────────────┬──────────────────────────────────┐  │
│  │  api key       │  provider                        │  │
│  ├────────────────┼──────────────────────────────────┤  │
│  │"anthropic-...  │ { stream, streamSimple }         │  │
│  │"openai-comp... │ { stream, streamSimple }         │  │
│  │"google-gen...  │ { stream, streamSimple }         │  │
│  │"bedrock-...    │ { lazy stream wrapper }          │  │
│  │ ...            │  ...                             │  │
│  └────────────────┴──────────────────────────────────┘  │
│                                                         │
│  stream(model, ctx) → resolveApiProvider(model.api)     │
│                     → provider.streamSimple(...)        │
└─────────────────────────────────────────────────────────┘

stream.ts 中的 streamSimple 是最常用的入口,它从注册表中找到对应 Provider 并调用其 streamSimple 方法:

// packages/ai/src/stream.ts
export function streamSimple<TApi extends Api>(
  model: Model<TApi>,
  context: Context,
  options?: SimpleStreamOptions,
): AssistantMessageEventStream {
  const provider = resolveApiProvider(model.api);
  return provider.streamSimple(model, context, options);
}

Bedrock 使用了懒加载策略——AWS SDK 体积庞大,只在实际使用时才动态导入:

function streamBedrockLazy(...): AssistantMessageEventStream {
  const outer = new AssistantMessageEventStream();
  loadBedrockProviderModule()
    .then((module) => {
      const inner = module.streamBedrock(model, context, options);
      forwardStream(outer, inner); // 转发事件到外部流
    })
    .catch((error) => {
      outer.push({ type: "error", ... });
    });
  return outer; // 立即返回,事件异步填充
}

EventStream:事件流引擎

EventStream<T, R> 是整个系统的信息传递骨干,实现了基于 AsyncIterator 协议的背压感知事件队列:

// packages/ai/src/utils/event-stream.ts
export class EventStream<T, R = T> implements AsyncIterable<T> {
  private queue: T[] = [];
  private waiting: ((value: IteratorResult<T>) => void)[] = [];
  private done = false;
  private finalResultPromise: Promise<R>;
  private resolveFinalResult!: (result: R) => void;

内部状态机

┌─────────────────────────────────────────────────────────────────┐
│                   EventStream 状态机                              │
│                                                                 │
│                        push(event)                              │
│                             │                                   │
│              ┌──────────────┼──────────────┐                   │
│              │              │              │                    │
│         有等待者?         有等待者?        队列中存储            │
│           (是)             (否)                                 │
│              │              │                                   │
│         直接唤醒          push to queue                         │
│              │                                                  │
│        isComplete?                                              │
│           (是)                                                  │
│              │                                                  │
│       resolveFinalResult()  ←── 终态                            │
│                                                                 │
│  ┌──────────────────────────────────────────────────────────┐  │
│  │            AsyncIterator 消费端                           │  │
│  │  for await (const event of stream) {                    │  │
│  │    queue.length > 0 → yield queue.shift()               │  │
│  │    done → return                                        │  │
│  │    else → await new Promise(resolve => waiting.push)    │  │
│  │  }                                                      │  │
│  └──────────────────────────────────────────────────────────┘  │
└─────────────────────────────────────────────────────────────────┘

核心方法详解

背压感知机制:当消费者处理速度慢于生产者时,queue 数组会累积事件;反之则 waiting 数组累积等待的 Promise。这种设计确保生产者不会无限堆积内存——消费者通过 for await 自然地控制消费节奏。

// AsyncIterator 实现:消费者 for-await 使用
[Symbol.asyncIterator](): AsyncIterator<T> {
  return {
    next: async (): Promise<IteratorResult<T>> => {
      // 情况一:消费者落后于生产者 —— 队列中已有事件
      // 场景:LLM Provider 推送事件的速度快于 UI 渲染速度
      // 结果:直接返回队列中的事件,无需阻塞
      if (this.queue.length > 0) {
        return { value: this.queue.shift()!, done: false };
      }
      
      // 情况二:流已终止 —— 不会再有新事件到来
      // 场景:LLM 响应完成,所有事件已被消费
      // 结果:发出迭代结束信号
      if (this.done) {
        return { value: undefined as any, done: true };
      }
      
      // 情况三:生产者落后于消费者 —— 暂时没有可用事件
      // 场景:消费者(UI)在 LLM 推送事件前调用了 next()
      //       或者网络延迟导致事件尚未到达
      // 结果:将 resolve 函数注册到等待队列,返回挂起的 Promise
      //       当 push() 被调用时,会找到该 resolve 并唤醒它
      return new Promise((resolve) => this.waiting.push(resolve));
    },
  };
}

关键设计result() 方法返回 finalResultPromise——消费者可以无需迭代所有事件,直接 await stream.result() 拿到最终结果。这在只需要完整响应、不关心流式过程时非常有用。

// 获取最终结果(无需迭代所有事件)
result(): Promise<R> {
  // 返回一个 Promise,当流接收到终止事件时完成。
  // 该 Promise 在 push() 内部被解析,当 isComplete(event) 返回 true 时:
  //   if (this.isComplete(event)) {
  //     this.done = true;
  //     this.resolveFinalResult(this.extractResult(event));
  //   }
  // 对于 AssistantMessageEventStream,这发生在 "done" 或 "error" 事件时。
  return this.finalResultPromise;
}

双返回类型 <T, R>T 是迭代事件类型,R 是最终结果类型。例如 AssistantMessageEventStream 迭代 AssistantMessageEvent,但 result() 返回 AssistantMessage,避免消费者为获取完整消息而手动收集所有事件。

AssistantMessageEventStream 是专用子类,其终止条件是 doneerror 事件,结果提取对应的 AssistantMessage

export class AssistantMessageEventStream 
    extends EventStream<AssistantMessageEvent, AssistantMessage> {
  constructor() {
    super(
      (event) => event.type === "done" || event.type === "error",
      (event) => {
        if (event.type === "done") return event.message;
        else if (event.type === "error") return event.error;
        throw new Error("Unexpected event type for final result");
      },
    );
  }
}

agent-loop:Agent 执行核心

agent-loop.ts 是整个项目中最精密的机制,418 行代码实现了完整的双层循环 Agent 执行模型。

双层循环架构

┌─────────────────────────────────────────────────────────────────────┐
│                      runLoop 双层循环架构                              │
│                                                                     │
│  ┌──────────────────────────────────────────────────────────────┐  │
│  │  外层循环:处理 follow-up messages(Agent 完成后续新任务)        │  │
│  │                                                              │  │
│  │  while (true) {                                              │  │
│  │    ┌──────────────────────────────────────────────────────┐ │  │
│  │    │ 内层循环:处理工具调用 + steering messages             │ │  │
│  │    │                                                      │ │  │
│  │    │  while (hasMoreToolCalls || pendingMessages) {        │ │  │
│  │    │                                                      │ │  │
│  │    │    [1] 注入 pendingMessages(用户中途注入)             │ │  │
│  │    │             ↓                                        │ │  │
│  │    │    [2] streamAssistantResponse()  ← LLM 调用          │ │  │
│  │    │             ↓                                        │ │  │
│  │    │    [3] 检查 stopReason(error/aborted → 退出)         │ │  │
│  │    │             ↓                                        │ │  │
│  │    │    [4] executeToolCalls() → 并发执行所有工具            │ │  │
│  │    │             ↓                                        │ │  │
│  │    │    [5] getSteeringMessages() → 检查中断                │ │  │
│  │    │  }                                                   │ │  │
│  │    └──────────────────────────────────────────────────────┘ │  │
│  │                                                              │  │
│  │    [6] getFollowUpMessages() → 是否有后续任务?               │  │
│  │        有 → 继续外层循环                                      │  │
│  │        无 → break,发送 agent_end                            │  │
│  │  }                                                           │  │
│  └──────────────────────────────────────────────────────────────┘  │
└─────────────────────────────────────────────────────────────────────┘

核心设计模式一览:

PatternImplementation
Dual LoopOuter (follow-up) + Inner (tool + steering)
Event-DrivenAsyncIterator-based EventStream<T,R>
Lazy LoadingBedrock provider loaded on first use
Type SafetyTypeBox runtime validation, zero any
Interruptibilitysteer() injects into steeringQueue → skip remaining tools
Pluggable TransportstreamFn: streamSimple | streamProxy

streamAssistantResponse:LLM 调用与上下文转换

这个函数是 AgentMessage[]Message[] 之间的边界:

async function streamAssistantResponse(
  context: AgentContext,
  config: AgentLoopConfig,
  signal: AbortSignal | undefined,
  stream: EventStream<AgentEvent, AgentMessage[]>,
  streamFn?: StreamFn,
): Promise<AssistantMessage> {
  // Step 1: transformContext(可选,用于剪枝/注入外部上下文)
  let messages = context.messages;
  if (config.transformContext) {
    messages = await config.transformContext(messages, signal);
  }

  // Step 2: convertToLlm(过滤 UI 专用消息,转换格式)
  const llmMessages = await config.convertToLlm(messages);

  // Step 3: 构建 LLM Context
  const llmContext: Context = {
    systemPrompt: context.systemPrompt,
    messages: llmMessages,
    tools: context.tools,
  };

  // Step 4: 调用 streamSimple,迭代事件流
  const response = await streamFunction(config.model, llmContext, {...});

  // Step 5: 将 AssistantMessageEvent 转发为 AgentEvent
  for await (const event of response) {
    switch (event.type) {
      case "start":
        context.messages.push(partialMessage);  // 立即加入上下文
        stream.push({ type: "message_start", message: {...partialMessage} });
        break;
      case "text_delta":
      case "thinking_delta":
      case "toolcall_delta":
        // 更新 partialMessage,发射 message_update
        stream.push({ type: "message_update", assistantMessageEvent: event, ... });
        break;
      case "done":
      case "error":
        stream.push({ type: "message_end", message: finalMessage });
        return finalMessage;
    }
  }
}

重点:partial message 在 start 事件时就立即加入 context.messages,后续 delta 事件直接原地更新(context.messages[context.messages.length - 1] = partialMessage),避免了数组频繁追加的开销,也确保了上下文的实时一致性。

executeToolCalls:工具执行与中断检测

┌─────────────────────────────────────────────────────────────────────┐
│                     工具调用执行流程                                   │
│                                                                     │
│  toolCalls = [tool_A, tool_B, tool_C]  (串行执行)                    │
│                                                                     │
│  for tool_A:                                                        │
│    emit tool_execution_start                                        │
│    validateToolArguments(TypeBox schema)                            │
│    result = await tool.execute(id, args, signal, onUpdate)          │
│    emit tool_execution_end                                          │
│    emit message_start / message_end (ToolResultMessage)             │
│    getSteeringMessages() ← 检查用户是否注入了方向盘消息               │
│      │                                                              │
│      ├── 有 steering → 跳过 tool_B, tool_C (skipToolCall)            │
│      │                steeringMessages 携带到下一轮                   │
│      │                                                              │
│      └── 无 steering → 继续 tool_B                                   │
│                                                                     │
│  skipToolCall:                                                      │
│    emit tool_execution_start (for skipped tool)                     │
│    result = "Skipped due to queued user message."                   │
│    emit tool_execution_end (isError=true)                           │
│    emit message_start / message_end                                 │
└─────────────────────────────────────────────────────────────────────┘

工具参数验证使用 TypeBox,在运行时执行 JSON Schema 验证:

const validatedArgs = validateToolArguments(tool, toolCall);
result = await tool.execute(toolCall.id, validatedArgs, signal, (partialResult) => {
  // 支持工具执行过程中的流式更新(如 shell 命令的实时输出)
  stream.push({ type: "tool_execution_update", partialResult });
});

Agent 类:高级状态管理封装

Agent 类是 agentLoop 的高层封装,维护完整的会话状态并提供响应式事件订阅:

┌─────────────────────────────────────────────────────────────────────┐
│                      Agent 状态机                                    │
│                                                                     │
│  AgentState {                                                       │
│    systemPrompt, model, thinkingLevel                               │
│    tools: AgentTool[]                                               │
│    messages: AgentMessage[]   ← 完整对话历史                         │
│    isStreaming: boolean                                              │
│    streamMessage: AgentMessage | null  ← 当前流式消息                │
│    pendingToolCalls: Set<string>                                     │
│    error?: string                                                   │
│  }                                                                  │
│                                                                     │
│  ┌─────────────────────────────────────────────────────────────┐   │
│  │                   消息队列双通道                               │   │
│  │                                                             │   │
│  │  steeringQueue  ────────────►  中途注入,跳过剩余工具调用       │   │
│  │  followUpQueue  ────────────►  Agent 完成后注入,触发新一轮     │   │
│  │                                                             │   │
│  │  steeringMode: "all" | "one-at-a-time"                     │   │
│  │  followUpMode: "all" | "one-at-a-time"                     │   │
│  └─────────────────────────────────────────────────────────────┘   │
│                                                                     │
│  用户 API:                                                          │
│    agent.prompt("text")     → 开始新一轮                             │
│    agent.steer(message)     → 注入方向盘消息                          │
│    agent.followUp(message)  → 注入后续消息                            │
│    agent.abort()            → 中止当前执行                            │
│    agent.waitForIdle()      → 等待执行完成                            │
│    agent.subscribe(fn)      → 订阅事件                               │
└─────────────────────────────────────────────────────────────────────┘

_runLoop 是实际执行方法,其关键部分是在 for await 循环中将 AgentEvent 映射到内部状态:

for await (const event of stream) {
  switch (event.type) {
    case "message_start":
      this._state.streamMessage = event.message;  // 更新流式消息引用
      break;
    case "message_update":
      this._state.streamMessage = event.message;  // 实时更新
      break;
    case "message_end":
      this._state.streamMessage = null;
      this.appendMessage(event.message);           // 持久化到历史
      break;
    case "tool_execution_start":
      this._state.pendingToolCalls.add(event.toolCallId);
      break;
    case "tool_execution_end":
      this._state.pendingToolCalls.delete(event.toolCallId);
      break;
    case "agent_end":
      this._state.isStreaming = false;
      break;
  }
  this.emit(event);  // 转发给所有订阅者
}

Proxy:透明代理支持

streamProxy 让 Agent 可以通过企业内网网关调用 LLM,而不暴露 API Key 给前端。

┌─────────────────────────────────────────────────────────────────────┐
│                       Proxy 流程                                    │
│                                                                     │
│  Client (Browser / App)           Proxy Server          LLM API    │
│         │                              │                    │      │
│         │  POST /api/stream            │                    │      │
│         │  {model, context, options}   │                    │      │
│         │  Authorization: Bearer xxx   │                    │      │
│         │─────────────────────────────►│                    │      │
│         │                              │ streamSimple(...)  │      │
│         │                              │───────────────────►│      │
│         │                              │◄── SSE events ─────│      │
│         │                              │                    │      │
│         │  SSE: ProxyAssistantMsg      │  (带宽优化:去除     │      │
│         │  (delta only, no partial)    │   partial 字段)     │      │
│         │◄─────────────────────────────│                    │      │
│         │                              │                    │      │
│  processProxyEvent()                   │                    │      │
│    → 客户端重建 partial message         │                    │      │
│    → 推入本地 EventStream              │                    │      │
└─────────────────────────────────────────────────────────────────────┘

Proxy 事件类型(ProxyAssistantMessageEvent)与直连模式的 AssistantMessageEvent 关键区别:去掉了每个 delta 事件中的 partial 字段,因为 partial 随着内容增长会越来越大,在 SSE 传输中会造成显著带宽浪费。客户端的 processProxyEvent 函数在本地重建 partial:

case "text_delta": {
  const content = partial.content[proxyEvent.contentIndex];
  if (content?.type === "text") {
    content.text += proxyEvent.delta;  // 客户端增量拼接
    return { type: "text_delta", ..., partial };
  }
}

完整交互流程分析

以"用户发送一条消息,Agent 调用工具后回复"为例,从调用 agent.prompt() 到事件到达 UI 订阅者的完整链路:

┌─────────────────────────────────────────────────────────────────────┐
│                        完整调用链路时序图                              │
│                                                                     │
│  User Code                Agent              agent-loop             │
│     │                       │                    │                  │
│     │ agent.prompt("help")  │                    │                  │
│     │──────────────────────►│                    │                  │
│     │                       │ _runLoop(msgs)      │                  │
│     │                       │───────────────────►│                  │
│     │                       │                    │ agentLoop()      │
│     │                       │                    │  push agent_start│
│     │                       │                    │  push turn_start │
│     │◄── event: agent_start ─────────────────────│                  │
│     │◄── event: turn_start  ─────────────────────│                  │
│     │◄── event: message_start(user) ─────────────│                  │
│     │◄── event: message_end(user) ───────────────│                  │
│     │                       │                    │                  │
│     │                       │            streamAssistantResponse()  │
│     │                       │                    │ streamSimple()   │
│     │                       │                    │  ← pi-ai 调用    │
│     │                       │                    │                  │
│     │                       │       AssistantMessageEvent stream    │
│     │                       │                    │◄── start         │
│     │◄── message_start ──────────────────────────│                  │
│     │                       │                    │◄── text_delta    │
│     │◄── message_update ─────────────────────────│ (流式文字)        │
│     │                       │                    │◄── toolcall_start│
│     │◄── message_update ─────────────────────────│                  │
│     │                       │                    │◄── done          │
│     │◄── message_end ────────────────────────────│                  │
│     │                       │                    │                  │
│     │                       │            executeToolCalls()         │
│     │◄── tool_execution_start ───────────────────│                  │
│     │                       │                    │ tool.execute()   │
│     │◄── tool_execution_update ──────────────────│ (进度流)          │
│     │◄── tool_execution_end ─────────────────────│                  │
│     │◄── message_start(toolResult) ──────────────│                  │
│     │◄── message_end(toolResult) ────────────────│                  │
│     │                       │                    │                  │
│     │                       │            (再次 streamAssistantResponse)
│     │◄── turn_end ───────────────────────────────│                  │
│     │◄── turn_start ─────────────────────────────│                  │
│     │                   ... (第二轮 LLM 调用) ...  │                  │
│     │◄── agent_end ──────────────────────────────│                  │
│     │                       │◄── stream.result() │                  │
│     │                       │ appendMessage(...)  │                  │
│     │                       │ isStreaming = false │                  │
└─────────────────────────────────────────────────────────────────────┘

AgentMessage 与 LLM Message 的双轨并行

┌─────────────────────────────────────────────────────────────────────┐
│              双轨消息体系:内部视图 vs LLM 视图                         │
│                                                                     │
│  context.messages (AgentMessage[])   llmMessages (Message[])        │
│  ┌─────────────────────────┐         ┌────────────────────────┐    │
│  │  UserMessage            │ ─────── │  UserMessage           │    │
│  │  AssistantMessage       │ ─────── │  AssistantMessage      │    │
│  │  ToolResultMessage      │ ─────── │  ToolResultMessage     │    │
│  │  NotificationMessage    │ ─ 过滤 ─│  (不传给 LLM)           │    │
│  │  ArtifactMessage        │ ─ 过滤 ─│  (不传给 LLM)           │    │
│  └─────────────────────────┘         └────────────────────────┘    │
│              │                                                      │
│     convertToLlm() 边界                                             │
│     (每次 LLM 调用前执行)                                             │
└─────────────────────────────────────────────────────────────────────┘

通过 TypeScript 的 Declaration Merging,应用层可以透明地扩展 AgentMessage 类型而无需修改核心库:

// 应用代码中
declare module "@mariozechner/pi-agent-core" {
  interface CustomAgentMessages {
    artifact: ArtifactMessage;
    notification: NotificationMessage;
  }
}
// AgentMessage 自动变为 Message | ArtifactMessage | NotificationMessage

设计亮点总结

1. 端到端流式架构

LLM Provider SSE → AssistantMessageEventStream
                 → AgentEvent (agent-loop)
                 → subscriber callbacks (Agent class)
                 → UI re-render

没有中间 Promise 打断流,整个链路零轮询。

2. Steering 中断机制

用户输入 "停!改一下方向"
    │
    ▼ agent.steer(message)
    └─► steeringQueue.push(message)
            │
            ▼ (当前工具执行完毕后)
        getSteeringMessages()
            │
            ▼ skipToolCall × N (跳过剩余工具)
            │
            ▼ 下一轮 LLM 调用携带 steering message

这使得 Agent 在执行长耗时工具链时可以被人类"掌舵",避免了"失控执行"问题。

3. 上下文管道

AgentMessage[]transformContext()   (剪枝、注入外部知识)
    → convertToLlm()       (格式转换、过滤 UI 消息)
    → Message[]
    → LLM Provider

两步管道清晰分离了"业务级消息变换"与"格式级消息转换"。

4. 声明式工具定义

const readFileTool: AgentTool<typeof ReadFileParams> = {
  name: "read_file",
  description: "Read file contents",
  label: "Reading file",
  parameters: ReadFileParams,  // TypeBox schema
  execute: async (id, { path }, signal, onUpdate) => {
    // TypeBox 在 executeToolCalls 中已验证 args 类型
    const content = await fs.readFile(path, "utf-8");
    return {
      content: [{ type: "text", text: content }],
      details: { path, size: content.length },
    };
  },
};

工具定义、参数验证、UI 标签、执行逻辑全部内聚在单一对象中。

5. 可替换的流函数

// 直连模式(默认)
const agent = new Agent({ streamFn: streamSimple });

// 代理模式(企业内网)
const agent = new Agent({
  streamFn: (model, context, options) =>
    streamProxy(model, context, {
      ...options,
      authToken: await getToken(),
      proxyUrl: "https://internal-gateway.corp",
    }),
});

streamFn 是一个普通函数类型,无需继承或实现接口,完全符合开闭原则。



读完之后

读这份代码库,我反复被一件事打动:它在任何地方都没有做"多余的事"。

EventStream 没有提供 mapfiltermerge 这些响应式操作符,因为 for await 已经够用。agent-loop.ts 没有实现"多 Agent 协作",因为那是上层应用该解决的问题。AgentTool 没有注册中心,因为一个普通数组已经足够。

这种克制在当今 AI 框架领域很罕见。LangChain 在构建之初就试图覆盖所有场景,结果是一个庞大的抽象层迷宫,初学者难以入门,高级用户又深陷配置地狱。Pi Monorepo 走了一条相反的路:只构建可以被清晰推理的核心,把"更多功能"的空间留给使用者。

当然,这也意味着它目前还不是一个"开箱即用"的完整框架——没有内置的 RAG 管道、没有向量数据库集成、没有可视化调试工具。但对于一个真正理解自己在构建什么的团队来说,这恰恰是优点:你能看清楚每一行代码在做什么,你能在出问题时准确定位,你不会在某个隐藏的中间件里迷失。

如果你正在用 TypeScript 构建一个需要长期维护的 AI Agent 系统,这份代码库值得认真读一遍。不只是为了用它,也为了学习这种"恰到好处"的工程风格。