请添加图片描述
本文分析 pi-mono 仓库中两个核心底层模块:

  • packages/agent:负责 Agent 运行时、消息状态和工具调用循环。
  • packages/ai:负责统一不同 LLM Provider 的调用方式和流式响应协议。

如果把 pi 看成一个 AI 编码助手,那么 coding-agent 是产品层,agent 是智能体运行层,ai 是模型接入层。

一、整体调用关系

典型运行链路如下:

用户 / Slack / Web UI / 其他上层入口
-> packages/coding-agent 或 packages/mom
-> packages/agent
-> packages/ai
-> OpenAI / Anthropic / Google / Mistral / Bedrock / 其他模型服务

可以用一句话概括:

packages/agent 决定 Agent 怎么运行;
packages/ai 决定模型请求怎么发出去、模型响应怎么统一回来。

两者的边界比较清楚:

  • agent 不关心 OpenAI、Anthropic、Google 的具体 API 差异。
  • ai 不关心 Agent 什么时候该继续、什么时候该执行工具。
  • 上层应用负责提供工具、会话、UI、鉴权和业务逻辑。

二、packages/agent:Agent 运行时

packages/agent 是通用 Agent Runtime。它不包含终端 UI,也不直接实现读文件、写文件、执行 Shell 这类业务工具。它只定义“工具应该长什么样”,并负责在模型要求调用工具时调度这些工具。

2.1 模块职责

packages/agent 主要负责:

  • 保存 Agent 状态:系统提示词、当前模型、思考级别、工具列表、消息历史、流式状态、执行中的工具调用和错误信息。
  • 接收新的用户输入,并启动一次 Agent 运行。
  • 从已有上下文继续执行。
  • 调用 packages/ai 获取模型的流式响应。
  • 识别模型返回的 tool call。
  • 校验工具参数并执行工具。
  • 把工具结果写回消息上下文。
  • 向上层 UI 或 Session 层发出生命周期事件。
  • 支持运行中插入 steering message 和运行结束前追加 follow-up message。

2.2 关键文件

  • packages/agent/src/index.ts:模块公共导出。
  • packages/agent/src/types.ts:核心类型定义,例如 AgentMessageAgentToolAgentEventAgentState
  • packages/agent/src/agent.ts:有状态的 Agent 类,是上层最常用的入口。
  • packages/agent/src/agent-loop.ts:底层 Agent 循环,真正驱动“模型响应 -> 工具执行 -> 再次模型响应”。
  • packages/agent/src/proxy.ts:可选的代理流实现,用于把模型请求转发到自建服务。

2.3 Agent 类

Agentpackages/agent 对外最核心的类。上层一般通过它启动和控制一次 Agent 会话。

它提供的主要能力:

  • prompt(...):从文本或消息开始一次新的 Agent 运行。
  • continue():从当前消息历史继续执行。
  • abort():中断当前运行。
  • subscribe(...):订阅 Agent 生命周期事件。
  • steer(...):在当前 assistant turn 结束、工具执行完成后插入新消息。
  • followUp(...):在 Agent 本来准备停止时追加一条后续消息。
  • waitForIdle():等待当前运行和事件监听器全部结束。

Agent 自己维护状态,但真正的循环执行会委托给 agent-loop.ts 中的 runAgentLoop()runAgentLoopContinue()

2.4 Agent Loop 运行流程

agent-loop.ts 是 Agent 行为的核心。简化后的流程如下:

1. 接收用户消息,把它加入上下文。
2. 发出 agent_start、turn_start、message_start、message_end 等事件。
3. 如果配置了 transformContext,先转换 AgentMessage[]。
4. 把 AgentMessage[] 转成 packages/ai 能理解的 Message[]。
5. 调用 streamSimple() 或自定义 streamFn 请求模型。
6. 流式接收 assistant message。
7. 如果 assistant message 包含 toolCall,则进入工具执行。
8. 工具执行完成后,生成 toolResult message。
9. 把 toolResult 加回上下文,再次请求模型。
10. 如果没有 toolCall、steering message、follow-up message,则结束。
11. 发出 agent_end 事件。

关键设计点是:agent-loop 内部一直使用 AgentMessage[],只有在调用 LLM 的边界才转换成 packages/aiMessage[]

2.5 工具执行机制

工具由上层传入,类型是 AgentToolpackages/agent 不知道某个工具是不是“读文件”或“执行命令”,它只知道这个工具有名字、参数 schema 和执行函数。

一个工具通常包含:

  • name:模型调用时使用的工具名。
  • description:工具描述。
  • parameters:参数 schema。
  • label:给 UI 展示的人类可读名称。
  • execute(...):真正执行工具逻辑。
  • prepareArguments(...):可选,用于在参数校验前修正模型给出的原始参数。
  • executionMode:可选,用于指定工具是否必须顺序执行。

工具执行期间会发出这些事件:

tool_execution_start
tool_execution_update
tool_execution_end

默认情况下,同一个 assistant message 里的多个 tool call 可以并行执行。如果全局配置为顺序执行,或者某个工具声明 executionMode: "sequential",则会按顺序执行。

三、packages/ai:统一 LLM 调用层

packages/ai 是模型适配层。它不运行 Agent,也不执行工具;它负责把不同模型服务的 API 抽象成一套统一接口。

不同厂商的 API 差异很大,例如:

  • 消息格式不同。
  • tool call 格式不同。
  • reasoning / thinking 的表达方式不同。
  • 流式事件协议不同。
  • usage 和 cost 统计不同。
  • 图片、缓存、headers、OAuth、API key 处理方式不同。

packages/ai 的作用就是把这些差异收敛起来,让上层只面对一套统一协议。

3.1 模块职责

packages/ai 主要负责:

  • 定义统一的模型、消息、工具、内容块、用量和流式事件类型。
  • 提供 stream()complete()streamSimple()completeSimple()
  • 注册内置 Provider。
  • 懒加载各 Provider 实现。
  • 提供模型查询能力,例如 getModel()getModels()getProviders()
  • 从环境变量或其他凭据来源解析 API key。
  • 把各 Provider 的流式响应转换成统一的 AssistantMessageEventStream
  • 处理跨 Provider 兼容问题,例如图片降级、thinking block 转换、tool call id 归一化等。

3.2 关键文件

  • packages/ai/src/index.ts:模块公共导出。
  • packages/ai/src/types.ts:统一类型定义,是理解该模块的入口。
  • packages/ai/src/stream.ts:对外的模型调用入口。
  • packages/ai/src/api-registry.ts:API Provider 注册表。
  • packages/ai/src/models.ts:模型查询和 cost 计算。
  • packages/ai/src/models.generated.ts:自动生成的内置模型元数据。
  • packages/ai/src/env-api-keys.ts:环境变量和凭据检测。
  • packages/ai/src/providers/register-builtins.ts:内置 Provider 的注册和懒加载。
  • packages/ai/src/providers/*:各模型厂商的具体适配实现。

3.3 统一消息协议

核心类型在 types.ts 中。

统一消息分为三类:

  • UserMessage:用户消息。
  • AssistantMessage:模型返回的 assistant 消息。
  • ToolResultMessage:工具执行结果消息。

Assistant 消息内容又分为:

  • text:普通文本。
  • thinking:推理或思考内容。
  • toolCall:模型要求调用工具。

统一流式事件包括:

start
text_start / text_delta / text_end
thinking_start / thinking_delta / thinking_end
toolcall_start / toolcall_delta / toolcall_end
done
error

这套协议的意义在于:无论底层是 OpenAI、Anthropic、Google 还是 Bedrock,上层 packages/agent 都可以用同一种方式消费模型响应。

3.4 stream 与 streamSimple

stream.ts 提供两层接口:

  • stream(model, context, options):更底层,面向 Provider 特定参数。
  • streamSimple(model, context, options):更通用,面向 Agent 和应用层。

packages/agent 默认调用的是 streamSimple()

streamSimple() 支持的通用配置包括:

  • apiKey
  • temperature
  • maxTokens
  • reasoning
  • thinkingBudgets
  • sessionId
  • cacheRetention
  • headers
  • onPayload
  • onResponse
  • signal

调用分发流程如下:

streamSimple(model, context, options)
-> 根据 model.api 找到对应 Provider
-> 调用 provider.streamSimple(model, context, options)
-> Provider 适配器调用真实模型 API
-> 返回统一 AssistantMessageEventStream

3.5 Provider 注册机制

api-registry.ts 维护一个注册表:

api 名称 -> Provider 实现

内置映射大致如下:

openai-responses        -> providers/openai-responses.ts
openai-completions      -> providers/openai-completions.ts
anthropic-messages      -> providers/anthropic.ts
google-generative-ai    -> providers/google.ts
google-gemini-cli       -> providers/google-gemini-cli.ts
google-vertex           -> providers/google-vertex.ts
mistral-conversations   -> providers/mistral.ts
bedrock-converse-stream -> providers/amazon-bedrock.ts

providers/register-builtins.ts 会注册这些内置 Provider,并且使用懒加载方式加载具体实现。这样做可以避免一启动就加载所有厂商 SDK,也能减轻浏览器环境或非 Node 环境的兼容压力。

3.6 模型查询机制

models.tsmodels.generated.ts 初始化内置模型注册表。

常用方法:

  • getModel(provider, modelId):获取一个内置模型。
  • getModels(provider):获取某个 Provider 下的所有内置模型。
  • getProviders():获取所有内置 Provider。
  • calculateCost(model, usage):根据模型价格计算 cost。
  • supportsXhigh(model):判断模型是否支持 xhigh 推理级别。

需要注意的是,在 coding-agent 正常运行时,模型选择通常不直接用 getModel(),而是走 ModelRegistryModelRegistry 会基于 getModels()getProviders() 构建模型列表,并叠加自定义模型、配置覆盖和鉴权状态。

四、两个模块如何协作

一次典型 Agent 运行可以拆成两层:

packages/agent:
  管理消息上下文
  判断是否需要继续
  执行工具
  发出生命周期事件

packages/ai:
  选择 Provider
  构造模型请求
  调用真实模型 API
  把响应转成统一事件流

更完整的链路如下:

用户输入
-> 上层应用创建 UserMessage
-> Agent.prompt()
-> runAgentLoop()
-> convertToLlm()
-> streamSimple()
-> provider.streamSimple()
-> LLM Provider
-> AssistantMessageEventStream
-> Agent 识别 toolCall
-> 执行 AgentTool
-> 生成 ToolResultMessage
-> 再次请求模型
-> 输出最终 AssistantMessage

这也是为什么这两个模块可以复用到不同产品入口里:

  • coding-agent 用它们实现 pi CLI。
  • mom 用它们实现 Slack bot。
  • web-ui 可以复用它们的类型、模型调用和示例逻辑。

五、总结

理解这两个模块时,可以始终记住这个分工:

packages/agent
  负责 Agent 状态、运行循环、工具调度和事件通知。

packages/ai
  负责模型元数据、Provider 选择、真实 API 调用和响应归一化。

如果问题是“Agent 什么时候继续、什么时候调用工具、工具结果怎么进入上下文”,应该看 packages/agent

如果问题是“某个模型请求怎么发给 OpenAI/Anthropic/Google、流式响应怎么被解析”,应该看 packages/ai

这两个模块共同构成了 pi 编码助手的底层能力:agent 负责智能体行为,ai 负责模型连接。上层的 coding-agent 再把它们组合成一个可交互、可使用工具、可管理会话的 AI 编码产品。

Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐