从源码看 Agent 工程的两条路线,以及它们为什么根本不在同一层
如果只看 README,DeepSeek Harness 和 Pi 很容易被归为同一类产品:都能连接模型、调用工具、操作文件、执行命令、保存会话,也都强调扩展能力。
但把源码调用链展开以后,会看到两套明显不同的工程哲学:
DeepSeek Harness 首先是一个“插件化 Agent 平台”;Pi 首先是一个“可嵌入的轻量 Agent 内核与 Coding Agent 组件集”。
它们并不是简单的竞品关系。一个很有意思的源码事实是:DeepSeek Harness 自己的多 Provider 适配器 dsh-llm-pi-ai,底层就依赖 Pi 的 pi-ai。换句话说,DeepSeek Harness 可以把 Pi 的模型接入层装进自己的插件树里。
我这边也有一些AI Coding(SDD Agent AI工具等) 和 Node技术交流交流群,感兴趣的可以加微信 ikoala520 进群,一起学习,共同进步。
本文基于以下源码快照分析,而不是仅依据产品文档:
- DeepSeek Harness:commit
b150a551b8d4,版本0.1.1-rc.2,2026-08-21。 - Pi:commit
ccfe79ed2386,2026-08-27;当前 npm 包名为@earendil-works/*。
DeepSeek Harness 仍明确标注为 Developer Preview,官方提示会发生兼容性破坏。本文讨论的是这个源码快照体现出来的架构,不把尚未稳定的 API 当成长期承诺。
一、Harness 到底解决什么问题
模型 API 只负责“给定消息,生成下一段内容”。一个真正能完成编码任务的 Agent,还要在模型外部补齐很多东西:
- 把系统提示、上下文、工具 Schema 组装成请求;
- 解析模型输出中的 Tool Call;
- 校验参数,执行文件、终端、搜索、MCP 等工具;
- 把 Tool Result 写回上下文,再次请求模型;
- 处理取消、重试、审批、权限和沙箱;
- 保存会话,支持恢复、分支、压缩和重放;
- 把底层流式事件映射成 UI 能稳定渲染的消息、工具卡片和状态。
其中最小的闭环是:
模型请求 → 模型返回 Tool Call → 执行工具 → 写入 Tool Result → 再请求模型
驱动这个闭环的控制逻辑通常被称为 Agent Loop。而 Harness 一般比 Agent Loop 更大:它还需要承载会话、工具、权限、扩展、宿主接入和 UI 协议。
为了避免后面混淆,可以先建立四层概念:
| 层级 | 核心问题 | 典型内容 |
|---|---|---|
| Model Provider | 如何调用不同模型 | 鉴权、请求格式、流式协议、Reasoning、Usage |
| Agent Core | 如何让模型持续行动 | Agent Loop、Tool Call、上下文转换、停止条件 |
| Agent Harness | 如何让 Agent 可长期运行 | Session、恢复、权限、审批、扩展、可观测性 |
| Product Host | 如何形成具体产品 | Web/桌面 UI、工作空间、业务流程、插件管理 |
DeepSeek Harness 覆盖了从 Provider 到 Product Host 的大部分层次;Pi 则把能力拆成可以按需组合的包,重点落在 Provider、Agent Core 和 Coding Agent Runtime。
下面用一个贯穿案例理解两者:
用户说:“检查支付模块的失败用例,修改代码并运行测试;写文件和执行高风险命令前需要我确认。”
这个任务至少会经历一次模型请求、若干次读文件、一次写文件审批、一次测试命令,以及多轮模型继续推理。
二、DeepSeek Harness:把 Agent 产品拆成一棵插件树
2.1 它是 Web-first,但不是只有 Web UI
DeepSeek Harness 默认最显眼的入口是:
npx @deepseek-ai/dsh web
这很容易让人以为它是一个“开源 Web 聊天页面”。源码里的真实边界要大得多。
DeepSeek Harness 用 Profile 表达一套可运行的产品组合,官方自带 web 和 headless 模板:
webProfile 在基础能力上增加浏览器应用;headlessProfile 提供没有服务器的一次性执行入口;- 两者都建立在共享的
dsh-baseBundle 上。
因此 Web 只是一个宿主形态,Agent、Session、Tool、LLM、Sandbox 并不属于 React 页面本身。这个分离很重要:同一套 Runtime 可以被 Web、CLI、测试程序,甚至未来的桌面壳驱动。
2.2 Cordis:不是“支持插件”,而是“万物皆插件”
DeepSeek Harness 的架构文档直接写道:Everything is a plugin。
它底层使用 Cordis。插件向共享 Context 注册三类东西:
- Service:例如
ctx.sessions、ctx.tools、ctx.llm; - Typed Event:例如
session/event、agent/pre-step、tools/pre-execute; - Reversible Effect:插件卸载时,注册行为能够反向撤销。
这里的关键不是“能额外装几个工具”,而是模型适配器、工具注册表、Session Log、Agent Loop 乃至 Web UI 本身都是插件。官方架构说明甚至强调,没有一个需要修改的“特权核心”;扩展行为的正常方式,是把新插件挂到已有插件旁边。
Profile、Bundle、Plugin Tree
DeepSeek Harness 的启动组合可以理解为三层:
Profile决定要叠加哪些 Bundle,例如web或headless;Bundle提供一组 Cordis 配置行和对应插件;- 用户的
cordis.patch.yml可以按 id 替换配置行或插入新行。
在
dsh-base 的默认配置中,可以直接看到 Session、Agent、LLM、JSONL 持久化、Sandbox、Approval、Permission Presets、Bash、文件工具、Skill 等插件。多 Provider 的 llm-pi-ai 也被默认挂载,只是在没有用户 Provider 配置时处于 dormant 状态。
这意味着所谓“替换某一部分”不是架构愿景,而是实际的装配机制。例如:
- 增加模型 Provider:向
ctx.llm注册 Adapter; - 改造文件系统:替换
ctx.fsProvider; - 把本地 Bash、PTY、LSP 一起迁入远端 Sandbox:替换共享执行世界的能力 Provider;
- 增加 UI 卡片:注册 Conversation Node Definition 和对应 Renderer;
- 拦截请求或工具:监听对应 Waterfall Event。
2.3 Agent Loop:Turn 与 Step 是两层状态机
DeepSeek Harness 对执行粒度做了清楚区分:
Step:一次模型请求,加上这次响应触发的工具执行;Turn:从一次输入被认领开始,到系统不再欠任何后续工作为止,可包含 0 到多个 Step。
以“修改支付模块并运行测试”为例,第一次模型请求可能调用 read;读取结果回填后,第二次请求调用 edit;审批并修改后,第三次请求调用 bash;测试结果回填后,第四次请求才给出总结。这是一个 Turn、四个 Step。
sequenceDiagram
participant U as User / Web
participant A as Agent Inbox
participant L as Agent Loop
participant M as LLM Adapter
participant T as Tool Pipeline
participant S as Session Log
U->>A: prompt / steer / queue
L->>S: turn/start
loop 一个 Turn 内可以有多个 Step
L->>S: step/start + user/message
L->>M: 组装历史、System Prompt、Tool Schema
M-->>L: assistant/chunk*
L->>S: assistant/chunk* + assistant/message
opt 模型返回 Tool Call
L->>S: tool/call
L->>T: pre-execute → execute → post-execute
T-->>L: Tool Result
L->>S: tool/result
end
L->>S: step/end
end
L->>S: turn/end
源码中,agent/pre-step 可以重写或拒绝将进入模型的消息;agent/request 和 llm/stream 可以拦截请求链路;agent/turn-stopping 则是最终停止点。
这套设计带来的价值是:压缩、重试、目标继续、上下文注入等能力不必全部硬编码到循环主体里,它们可以在明确的生命周期节点上参与决策。
有一个容易踩坑的术语差异:**DeepSeek 的 Step,大致对应 Pi 事件语义里的一个 turn_start → 模型 → 工具 → turn_end;DeepSeek 的 Turn 则可能包住多个这样的 Step。**所以不能只按名字把两边的 Turn 一一对应。
2.4 Tool Pipeline:工具调用不是一个 execute() 就结束
DeepSeek Harness 的工具执行被拆成一条受控流水线:
这里有几个工程细节值得关注:
第一,Tool Call 在执行前就被记录,因此 UI 可以先出现“等待执行”的卡片,崩溃恢复时也知道系统停在什么位置。
第二,权限与审批不是散落在每个工具里的 if。tools/pre-execute、单调 Guard、ctx.approval、Sandbox 和 tools/post-execute 共同形成治理链。
第三,审批采用 fail-closed:需要审批但没有可用回答者时,不会悄悄放行。
第四,工具最终结果经过归一化后再成为唯一的模型可见 tool/result,避免模型上下文、持久化记录和 UI 展示各有一套结果。
这也是 DeepSeek Harness 比普通 Tool Calling SDK 更像“平台”的地方:它不只解决“工具怎么调”,还解决“工具在什么权限下调、谁能拦截、怎样恢复、怎样展示”。
2.5 SessionEvent:把会话当作事件源,而不是消息数组
DeepSeek Harness 的 Session 是 append-only 的 SessionEvent 日志。核心事件包括:
turn/start、turn/end;step/start、step/end;user/message;assistant/chunk、assistant/message;tool/call、tool/result;- 以及插件通过类型扩展增加的事件。
它有一句非常关键的设计约束:
Model-visible means logged——只要内容会进入模型,就必须能从日志重建。
因此模型历史不是另一份独立保存的 messages[],而是通过 deriveMessages() 从 Session Log 投影出来。会话恢复、Fork、Transcript、Telemetry、持久化和 UI 重放也都基于同一条事件流。
这解决了 Agent 产品常见的“多份真相”问题:模型看到一份消息,Web 保存一份消息,工具卡片又维护另一份状态,断线恢复后彼此对不上。事件源设计把“发生过什么”固定下来,再允许不同消费者形成各自视图。
代价也很明确:事件协议需要版本治理;投影器需要兼容历史事件;原始 chunk 会增加存储体积;任何新的模型可见输入都必须先定义可持久化事件,而不能临时塞进请求。
2.6 Web UI 并不只是渲染 SDK 返回值
DeepSeek Harness 的 Web 链路同样分层。简化后如下:
Client Session.prompt()
→ API Proxy / RPC
→ Host Agent Inbox 与 Agent Loop
→ SessionEvent
→ Mux / Host Stream
→ Client Session
→ ConversationNodeAssembler
→ ConversationSnapshot / keyed views
→ React UI
浏览器侧的 Session 持有当前事件窗口、Pending Interaction、运行状态和可观察快照。ConversationNodeAssembler 不只是把 JSON 转为 JSX,而是把连续的原始事件窗口增量折叠成业务节点,再为不同 View 构建快照。
因此,同一条 SessionEvent 可以被投影为:
- 普通聊天消息;
- Assistant 流式节点;
- Tool Call/Result 卡片;
- Turn/Step 位置;
- Compaction 标记;
- 重试提示;
- 独立的 Trajectory 视图。
审批和用户提问则属于“等待人类响应的交互状态”。Host 通过带稳定 rpcId 的请求帧推送,Client 将其维护为 PendingInteraction;断线重连时,Mux 会重放仍未解决的请求。这样“审批弹窗消失了,但后台还在等”不会成为默认行为。
所以 DeepSeek 的 Web 层也有一套会话映射和状态机,而且并不薄。它负责的是客户端事件窗口、业务投影、交互态和视图扩展;Agent 执行、工具权限、持久化真相仍属于 Host Runtime。
三、Pi:把最小 Agent Loop 做成可嵌入组件
3.1 Pi 不是一个单体 CLI
Pi Monorepo 的稳定主链路分为四层:
| 包 | 职责 |
|---|---|
pi-ai | 统一多 Provider 模型、鉴权、流式事件、Reasoning、Usage、Tool Call |
pi-agent-core | 有状态 Agent、Agent Loop、工具执行与事件流 |
pi-coding-agent | 编码工具、Session、压缩、重试、Extension、CLI/RPC/SDK |
pi-tui | 终端 UI 与增量渲染 |
这个分层使 Pi 同时具备两种身份:终端里,它是一个完整 Coding Agent;代码里,它又可以只作为 Provider 层或 Agent Core 被别的产品嵌入。
3.2 pi-ai:统一 Provider,不替模型做 Agent 决策
pi-ai 解决的是模型世界的差异:
- 不同 Provider 的模型目录和 API 类型;
- API Key、OAuth 等鉴权;
- SSE/WebSocket 等流式传输;
- 文本、Thinking、Tool Call 的增量事件;
- Context Window、Token Usage、Cost 等元数据;
- OpenAI、Anthropic、Google、DeepSeek、OpenRouter、本地 llama.cpp 等 Provider。
多 Provider 是 Pi 很突出的现成能力,但它不是 Agent Loop 本身。**Provider Adapter 解决协议差异,Agent Loop 解决控制流。**这两个概念不能合并。
3.3 pi-agent-core:核心就是一个清晰的双层循环
Pi 的 packages/agent/src/agent-loop.ts 很适合用来理解 Agent Loop,因为控制流相对直接。
它包含两个循环:
- 内层循环:只要还有 Tool Call 或 Steering Message,就继续下一轮模型调用;
- 外层循环:Agent 原本准备停止时,再检查是否有 Follow-up Message,有则重新进入内层循环。
一个 Tool Call 的处理过程也很规整:
- 根据名称查找工具;
prepareArguments预处理参数;- 按 Tool Schema 校验参数;
- 调用
beforeToolCall,允许扩展阻断; - 执行 Tool;
- 调用
afterToolCall,允许改写结果; - 发出
tool_execution_end; - 生成标准
ToolResultMessage并放回上下文。
同一条 Assistant Message 中的多个工具可以顺序执行,也可以并行执行。并行模式仍按模型原始顺序生成 Tool Result Message,保证回填上下文的顺序稳定。
Pi 还专门防御了一类细节问题:如果模型因为输出 Token 上限而截断,可能留下“碰巧可以解析但参数不完整”的 Tool Call。Pi 会把这批 Tool Call 全部标为失败,而不是冒险执行。
3.4 AgentMessage 与模型消息分离
Pi 允许应用定义自有 AgentMessage。但模型只认识标准的 User、Assistant 和 Tool Result,所以每次请求前会经过:
AgentMessage[]
→ transformContext()
→ AgentMessage[]
→ convertToLlm()
→ 标准 Message[]
→ Provider
transformContext 可以做裁剪、压缩或上下文注入;convertToLlm 可以过滤纯 UI 消息,或把业务消息转换成模型可见格式。
这是很实用的边界:会话里可以存在比模型消息更多的业务信息,但只有经过显式投影的内容才进入模型。
与 DeepSeek 相比,Pi 稳定主链路更偏“消息与状态对象驱动”,而不是把所有运行事实先统一为一份跨宿主的 SessionEvent 协议。
3.5 pi-coding-agent:在 Agent Core 上补齐产品能力
pi-agent-core 只提供通用循环。真正让 Pi 成为 Coding Agent 的是 pi-coding-agent:
- 默认提供
read、write、edit、bash; AgentSession连接 Agent 事件、Session 持久化、自动压缩、重试和扩展;AgentSessionRuntime管理切换会话、切换工作目录时的资源重建与清理;SessionManager管理 JSONL 会话树;ExtensionRunner分发扩展生命周期和工具钩子;- 支持 Interactive、Print/JSON、RPC、SDK 四种使用方式。
这说明 Pi 的价值不只是那几十行循环,而是围绕小内核形成了一套可嵌入的编码 Agent 运行时。
3.6 JSONL 会话树:天然支持分支
Pi 的 Session 也使用 JSONL,但结构与 DeepSeek 的顺序事件日志不同。
每个 Entry 有 id 和 parentId,所有 Entry 在一个文件里构成树;当前 leaf 指向正在使用的分支。追加消息就是在当前 leaf 下增加子节点,跳转历史节点后继续工作则形成新分支。
因此 Pi 可以自然支持:
/tree在同一文件中查看并切换分支;/fork从历史消息创建新 Session 文件;/clone复制当前活动路径;- Compaction 后保留原始历史,需要时仍可回到旧节点。
DeepSeek 更强调“完整执行事件可以重放和投影视图”,Pi 更强调“对话 Entry 的树形导航和轻量本地持久化”。两者都用 JSONL,不代表数据模型相同。
3.7 Extension:足够实用,但不是 Cordis 式“万物皆插件”
Pi Extension 可以注册:
- Tool、Command、Shortcut、Flag;
- Provider;
- 自定义消息和 Entry Renderer;
- 输入、上下文、模型请求、Tool Call、Tool Result、Session 生命周期等事件;
- TUI 状态栏、Widget、Overlay,甚至替换编辑器组件。
这已经是很强的扩展机制,而且 TypeScript 文件可以通过 Jiti 直接加载。对于个人工作流或团队 Coding Agent,它比重量级插件平台更容易上手。
但 Pi 的 Extension 主要围绕 pi-coding-agent 预设的扩展点工作。AgentSession、Session Manager、TUI 等仍然是明确的核心对象;它不像 Cordis 那样把 Agent Loop、Session 服务、LLM 服务和 UI 组件全部放进同一种可装卸插件模型。
可以简化为一句话:
Pi 是“小内核 + 丰富扩展点”;DeepSeek Harness 是“插件容器 + 由插件组成的产品”。
3.8 权限边界:Project Trust 不等于运行时 Sandbox
Pi README 明确说明:Pi 不内置限制文件系统、进程、网络或凭据访问的权限系统,默认继承启动它的用户与进程权限。需要强隔离时,应使用容器、微型虚拟机或外部 Sandbox。
Pi 有 Project Trust,用于决定是否加载项目本地设置、扩展和资源;但它解决的是“是否信任项目代码”,不等于每一次文件写入或命令执行的细粒度审批。
这与 DeepSeek Harness 默认组合中的 Sandbox、Approval、Permission Presets 有明显差异。
3.9 注意:Pi 新增的 AgentHarness 仍是演进中脚手架
当前 Pi 源码里已经出现新的 packages/agent/src/harness/agent-harness.ts,接口设计包含 durable Session、Lane、Run、Resume、Queue、Hook、Watch 等概念,看起来是在向更完整、可恢复的 Harness API 演进。
但在本文所用 commit 中,prompt、compact、resume、steer、watch 等大量关键方法仍返回 HarnessNotImplemented,恢复已有记录也尚未实现。
因此评估 Pi 时必须区分两件事:
- 已经稳定可用的主链路:
pi-ai、Agent/agentLoop、AgentSession、SessionManager、Extension; - 正在形成的新方向:durable
AgentHarnessAPI。
不能因为仓库中出现了 AgentHarness 类,就把规划中的接口当成已经可以替换生产 Runtime 的成熟实现。
四、本质差异:一个在设计“平台”,一个在设计“内核”
| 维度 | DeepSeek Harness | Pi |
|---|---|---|
| 首要定位 | 插件化 Agent 平台 | 可嵌入 Agent 内核 + Coding Agent |
| 核心抽象 | Cordis Context、Service、Event、Effect、Plugin Tree | Model、Agent、Agent Loop、AgentSession、Extension |
| 组合方式 | Profile + Bundle + Patch,几乎所有模块均为插件 | 分层 npm 包 + Coding Agent 扩展点 |
| Agent 循环 | Turn 包含 0~多个 Step,生命周期扩展点细 | 双层循环直接清晰,Steering/Follow-up 内建 |
| 会话真相 | append-only SessionEvent,模型历史与 UI 均从日志派生 | JSONL Entry 树,活动 leaf 决定上下文分支 |
| UI | Web Client Runtime 也插件化,事件组装为 Conversation Node/View | 以 TUI 为主,也提供 JSON、RPC、SDK 嵌入方式 |
| 工具治理 | Pre/Guard/Approval/Execute/Post/Sandbox 完整流水线 | before/after hooks、顺序/并行执行;隔离交给外部环境 |
| 多 Provider | 自有 DeepSeek Adapter,也可通过 llm-pi-ai 使用 Pi | pi-ai 是核心优势,Provider 范围广 |
| 插件强度 | 可替换基础服务和 Loop,支持可逆生命周期 | 擅长扩展 Tool、Provider、命令、事件和 TUI |
| 学习与改造成本 | 高,概念和组合机制较重 | 低到中,适合先嵌入再扩展 |
| 当前成熟度提醒 | Developer Preview,明确会 breaking change | 稳定主链路可用;新 durable AgentHarness 尚未完成 |
如果用建筑作比喻:
- Pi 提供发动机、变速箱和一辆可改装的轻型车;
- DeepSeek Harness 提供一套模块化底盘、总线、装配规范和已有驾驶舱。
因此,问“DeepSeek Harness 和 Pi 哪个更好”并不完整。更好的问题是:
我们缺的是一个可控的 Agent Loop,还是一个能持续装配多类 Agent 产品的插件宿主?
五、一个容易被忽略的事实:DeepSeek Harness 正在使用 Pi
DeepSeek Harness 的 @deepseek-ai/dsh-llm-pi-ai 包依赖:
"@earendil-works/pi-ai": "^0.82.1"
在默认 dsh-base Bundle 里,这个插件以 dormant 状态挂载;当用户在设置中配置 Provider Profile 时,它动态注册相应模型路由。
这揭示了两者最真实的关系:
DeepSeek Harness
负责插件装配、Agent 生命周期、SessionEvent、工具治理、Web 投影
↓
dsh-llm-pi-ai Adapter
↓
Pi pi-ai
负责多 Provider、模型目录、鉴权和流式协议
所以“用 DeepSeek 还是用 Pi”可能是假二选一。一个合理系统完全可以同时借用两者处在不同层级的能力。
总结:我们要靠近的是思想,不是仓库形状
DeepSeek Harness 最有价值的地方,不是它恰好有一个 Web UI,而是它把 Agent 产品理解为可组合的插件树,并用 SessionEvent 和 Projection 连接 Runtime 与界面。
Pi 最有价值的地方,也不只是原生支持很多 Provider,而是它把模型访问、Agent Loop、Coding Agent 产品能力拆成不同包,让宿主可以只拿自己需要的层。
我这边也有一些AI Coding(SDD Agent AI工具等) 和 Node技术交流交流群,感兴趣的可以加我微 ikoala520 进群,一起学习,共同进步。
源码索引
DeepSeek Harness
- 仓库:deepseek-ai/deepseek-harness
- 架构总览:
architecture.md - Agent Turn/Step:
agent-lifecycle.md - Tool Pipeline:
tool-execution-pipeline.md - Session:
session.md - 默认 Bundle:
cordis.patch.yml - Pi Provider Adapter:
llm-pi-ai/package.json - Web Client Session:
session.ts - Conversation Assembler:
conversation-assembler.ts
Pi
- 仓库:badlogic/pi-mono
- Agent Loop:
agent-loop.ts - Agent Core 类型:
types.ts - Coding Agent:
coding-agent/README.md - Session 格式:
session-format.md - Extension:
extensions.md - AgentSession:
agent-session.ts - 新 AgentHarness 脚手架:
agent-harness.ts