它想回答一个更实际的问题:当你的 Agent 从"调用一次模型"长成"能读文件、跑命令、调用浏览器、记住历史、等待审批、拆分子任务"的系统之后,怎么避免把所有能力都塞进同一个
while循环?
开篇:Agent 项目最容易长成一团什么
很多工程师第一次做 Agent,起点都很合理。
先把用户的问题发给大模型;如果模型要求调用工具,就执行工具;再把结果交回模型,直到它给出最终答案。几十行代码,一个可用的 Agent 就跑起来了。
messages = [user_message]
while True:
reply = llm.chat(messages, tools=tools)
if not reply.tool_calls:
return reply.text
messages.append(reply)
for call in reply.tool_calls:
result = run_tool(call)
messages.append(result)
真正麻烦的事情,通常从第二周才开始出现。
删除文件前要不要让用户确认?Shell 命令该在本机跑,还是在远程沙盒跑?工具执行超时了,应该重试还是停止?用户刷新网页以后,刚才的对话怎么恢复?一个复杂任务要不要交给子 Agent?工具返回了两万字,下一轮还要原样塞进上下文吗?
如果这些判断都继续堆进上面的循环,run_tool() 会慢慢变成权限中心,messages 会慢慢变成数据库,模型调用处会慢慢变成重试器、监控器和工作流引擎。
最后,它看起来仍是一段 Agent 代码,实际上已经是一套没有边界的运行时。
DeepSeek Harness 选择了另一条路:让 Agent Loop 只负责驱动,让其他能力以插件的方式接进来。
这里的"一切皆插件",不是说每个函数都要拆成一个 npm 包。
它真正想表达的是:会独立变化的能力,应该拥有独立的接口、生命周期和扩展位置。

接下来,我们就沿着一次真实任务的路径,理解这套架构为什么要这样设计。
PART 01:先把地图摊开,一次 Agent 任务究竟经过了什么
01 Agent SDK 和 Agent Harness,差别不在"能不能调模型"
很多人把 Agent 框架理解成"帮我把 function calling 封装一下"。这没有错,但只描述了最外层。
一个模型 SDK 主要解决的是:怎样把消息发给模型,怎样读取流式输出,怎样声明工具 schema。
而一个 Agent Harness,也就是 Agent 运行时底座,还要解决下面几类问题:
- 模型路由:这次任务用哪个模型,模型配置变更后怎样生效;
- 上下文组装:系统提示词、历史消息、工具定义、记忆内容该怎样拼;
- 工具执行:模型调用工具后,如何校验参数、并行调度、处理异常;
- 状态持久化:进程重启、浏览器刷新或任务恢复后,如何找回同一个工作现场;
- 安全治理:文件写入、命令执行、网络访问,哪些自动放行,哪些必须审批;
- 交互与协作:Web、命令行、自动化协议、子 Agent 如何共用同一套任务状态。
可以把 SDK 理解成一台性能很好的发动机。
Harness 则是把发动机、仪表盘、刹车、座椅、安全带和道路规则装配成一辆能长期上路的车。这个类比只帮助建立直觉,技术上它们分别对应模型适配、会话、工具、策略、界面和生命周期管理。
DeepSeek Harness 的核心想法是:不要把这些职责偷偷塞进"调用模型"的函数里,而要让它们在运行时有明确位置。
02 一张总图:从配置到模型,再回到状态
先看全局。下面这五层并不是一次任务严格串行经过的五个函数,而是同一套系统的不同视角;它们都由插件装配,外层的插件运行时也不会把 Agent Loop 变成不可替换的特权核心。
- 组合层决定"这次启动装哪些能力"。它由 Profile、Bundle 和配置覆盖组成。
- 插件运行时层负责加载插件、提供服务、派发事件和在卸载时清理注册项。
- 核心驱动层包含 Agent、Agent Loop、提示词组装、工具注册表、模型服务和会话。
- 能力层提供可替换实现,例如 DeepSeek 模型、文件系统、Shell、沙盒、审批、搜索、子 Agent、工作流。
- 状态与交互层把过程写入 Session Event Log,再投影到持久化、Web UI、CLI、自动化客户端和遥测。

如果用户在 Web 页面输入"帮我检查这个项目的测试失败原因",路径大致是这样的:
用户输入
-> Agent 收到待处理消息
-> Agent Loop 组装系统提示词、可见工具和历史
-> LLM 生成回答或 tool call
-> 工具管线检查权限并执行
-> 工具结果写入会话
-> Agent Loop 决定是否继续下一步
-> 会话事件投影到页面、持久化和其他观察者
这里最重要的一点是:模型请求和工具执行不是整套系统唯一的主角。
配置决定能力集合,事件决定谁能观察过程,日志决定系统重启后还能记住什么。把这三件事看见以后,你就能理解为什么一个成熟的 Agent 系统不能只靠一条聊天数组支撑。
03 插件运行时的五个基础概念
DeepSeek Harness 建在 Cordis 之上。第一次接触时,Service、Context、Inject、Event、Effect 这些名字很容易让人觉得抽象。我们逐个翻译成工程里的问题。
Service:我向系统提供什么能力?
例如"模型调用""工具注册""会话管理"都可以是服务。服务先声明一个稳定入口,比如 ctx.llm 或 ctx.tools,其他代码只依赖这个入口,不直接绑定某一家实现。
Context:运行时从哪里拿能力?
Context 可以理解为当前 Agent 所在的"能力工作台"。一个插件要用模型服务,就从 ctx.llm 获取;要注册工具,就使用 ctx.tools。它不是全局变量的随意堆放处,而是有生命周期和作用域的能力容器。
Inject:插件启动前依赖什么?
假设一个插件想注册 DeepSeek 模型适配器,它首先需要 LLM 服务存在。inject 就是把这个前置条件说清楚。运行时根据服务是否就绪来激活插件,而不是让开发者手写一长串"先启动 A,再启动 B"。
Event:发生一件事时,谁可以参与?
模型请求前、工具执行前、工具执行后、一个 turn 即将结束时,系统都会提供具名事件。某些事件只是通知,某些事件允许插件包一层逻辑,甚至阻断后续执行。
其中一类重要事件叫 Waterfall ,可以把它理解成环绕式中间件:监听器调用 next(),处理权才会交给后续插件;监听器直接返回,则表示它接管当前决定并截断后续链路。它适合审批、改写请求和结果治理这类需要"继续还是终止"判断的场景。
Effect:注册的东西怎样被收回?
插件装入时可能注册工具、提示词段、模型 Provider、定时器和事件监听器。它卸载时,这些贡献也必须一起撤销,否则热更新或切换配置后会出现重复工具、幽灵监听器和旧配置继续生效的问题。
下面是一个简化后的模型 Provider 注册示意。它不是完整生产代码,只保留架构关系:插件依赖抽象 LLM 服务,再把具体 Adapter 注册进去。
export const inject = ['llm']
export function apply(ctx: Context) {
const adapter = new DeepSeekAdapter()
ctx.llm.registerAdapter(['deepseek-official'], adapter)
}
Agent Loop 之后只面对统一的模型调用接口。今天是 DeepSeek,明天换成另一个 Provider,主循环不需要学会新的 HTTP 请求格式。

这一部分只记住三件事:
- Agent Harness 解决的是模型调用之外的长期运行问题。
- 一切皆插件的前提,是先把稳定服务入口和事件入口定义好。
- 插件化的价值不是"拆得更碎",而是让变化不必侵入 Agent Loop。
PART 02:系统怎样被组装,又怎样跑完一次任务
04 Profile 与 Bundle:同一套底座,为什么能跑成 Web 和命令行
一个很自然的问题是:Web 版 Agent、命令行 Agent 和无人值守的自动化 Agent,难道要各自维护一套核心代码吗?
DeepSeek Harness 的答案是不用。它把"应用长什么样"从代码分支中抽出来,变成可组合的配置层。
Profile 可以理解为一个运行配置方案。例如 web Profile 需要浏览器界面和 HTTP 服务,headless Profile 则只需要接收一条命令、运行任务并输出结果。
Bundle 则是一组可以叠加的能力配置。基础 Bundle 负责模型、会话、工具、安全策略等共用能力;不同产品形态在它上面再添加自己的部分。
配置覆盖大致按下面的顺序发生:
基础 Bundle
-> 产品形态 Bundle
-> Profile 自己的配置覆盖
-> 用户级配置覆盖
-> 命令行临时覆盖
这里的"覆盖"很重要。它让用户不需要改动框架源码,也能替换模型参数、挂入新的插件或关闭某项能力。
一个极简配置示意可能长这样:
- id: llm
name: '@deepseek-ai/dsh-llm'
- id: session
name: '@deepseek-ai/dsh-session'
- id: tools
name: '@deepseek-ai/dsh-tools'
- id: agent-loop
name: '@deepseek-ai/dsh-agent-loop'
这不是说配置文件比代码更神奇。
它表达的是一个很朴素的工程事实:"系统装了什么"本来就是部署决策,应该能被看见、被检查、被覆盖。
同时,这条路也有约束。DeepSeek Harness 的 patch 覆盖会替换目标配置行,而不是做隐式深度合并。好处是结果明确,坏处是覆盖者必须知道并重述自己想保留的字段。
这比"配置缺字段时悄悄补一个默认值"更麻烦,却能避免不同配置层悄悄拼出一个谁也说不清的最终状态。

05 Agent Loop:把循环压缩到最小,但不把能力做薄
很多入门教程会用一个 while True 写完 Agent。这个写法并不幼稚,它抓住了 Agent 的基本节奏:模型回答,工具执行,再让模型继续。
DeepSeek Harness 没有否定这个节奏,而是把它拆成三个便于观察的时间单位。
- Turn:驱动器处理一批待办输入的一次完整工作回合,可以包含零个或多个 Step。
- Step:Turn 内的一次模型请求,以及这次请求触发的工具调用。
- Tool Call:模型要求执行的一个具体工具动作。
举个例子。用户说:"读取报错日志,定位失败测试,然后修复问题。"
模型第一次请求工具读取日志,这是 Step 1;模型看到日志后又调用搜索和读文件工具,这是 Step 2;模型修改代码并运行测试,可能是 Step 3。所有这些 Step 共同组成一个 Turn。
简化后的主流程可以写成:
while (agent.hasPendingInput()) {
const turn = agent.openTurn()
while (turn.needsAnotherStep()) {
const input = agent.claimInput()
const prompt = assembleSystemPromptAndTools(agent)
const history = session.deriveMessages()
const reply = await llm.stream({ prompt, history })
session.record(reply)
if (reply.hasToolCalls()) {
await tools.execute(reply.toolCalls)
}
}
agent.closeTurn()
}
真实实现当然还要处理取消、并发工具、异常、恢复和流式分片。但从架构上看,Agent Loop 只需要守住这一小段骨架。
为了建立直觉,可以先看两个最直观的开放位置;真实实现还在 agent/pre-step、agent/request、agent/request-error、llm/stream 和 agent/turn-stopping 等事件上继续开放:
- 进入模型前:插件可以补充或拒绝本次输入,例如注入任务上下文、应用计划模式、触发压缩。
- 执行工具时:插件可以进行审批、沙盒限制、超时控制、结果加工和观测。
这就是"核心循环保持小"的真实含义。不是减少功能,而是让功能从循环外部通过明确的入口参与进来。

06 Prompt 和 Tool 不是两张写死的数组
刚开始写 Agent 时,System Prompt 往往是一大段字符串,工具往往是一个全局数组。
这种写法在 Demo 阶段没有问题。但当不同用户、不同工作区、不同模式需要不同能力时,问题就出现了。
例如,一个只读分析任务不应该看到"删除文件"工具;一个计划阶段可能需要禁止真正执行命令;一个子 Agent 也许只需要搜索和读取能力,不应该继承父 Agent 的全部工具。
所以,DeepSeek Harness 把 Prompt 看成多个具名 section 的组合,把工具看成当前 Agent 作用域内可见定义的集合。
你可以把它理解成一张任务前的"工作台清单":这次任务有哪些规则、能调用哪些工具、能访问哪些上下文,都在发给模型前统一装配。
这还带来一个经常被忽略的性能点:模型请求的前缀越稳定,KV Cache 越容易复用。
KV Cache 是大模型推理中保存历史 Key 和 Value 的缓存。对 Agent 而言,如果每一步都随意重排提示词和工具 schema,虽然语义差不多,但 token 序列变了,缓存命中就会变差。
因此,"插件可以动态装卸"不等于"每次请求都随机变化"。好的架构会把稳定性也作为组合规则的一部分。
这一部分只记住三件事:
- Profile 和 Bundle 把部署形态从业务代码中分离出来。
- Agent Loop 负责推动任务前进,不负责亲自实现每一种能力。
- Prompt 和 Tools 是本次任务的运行时装配结果,不是永远不变的全局常量。
PART 03:为什么状态、安全和 Provider 替换,才是 Agent 工程的分水岭
07 Event Log:为什么聊天记录不够用
不少 Agent 项目把状态存在一个 messages: Message[] 数组里。只要页面不刷新、进程不重启,这很方便。
但一旦需要恢复、分叉、回放或审计,普通聊天数组就开始不够了。
假设模型正在流式输出,网页需要一边显示 token,一边在最终完成后保存消息;工具执行前发生了用户审批;一个子 Agent 从父任务的某个历史位置开始工作;工具结果太大,被截成预览、保存到 spill 文件,或在 compaction 阶段做首尾剪枝。只保存最后的聊天文本,很难解释这些事情是怎么发生的。
DeepSeek Harness 使用 Session Event Log,中文可以叫"会话事件日志"。它不是普通调试日志,而是一串按顺序追加的事实。
turn/start
step/start
user/message
request/header(首次、恢复或配置变化时)
request/context(发生变化时)
assistant/chunk ...
assistant/message
tool/call
tool/result
step/end
turn/end
这里的关键不是事件名称,而是它带来的推导能力。
- 模型历史可以从事件中投影出来;
- 页面可以从同一份事件中恢复流式输出和工具卡片;
- 持久化层可以把事件写入 JSONL 或 SQLite;
- 会话 fork 可以复制某个历史边界之前的事件;
- 遥测可以观察延迟、错误和工具活动,而不需要再往主循环里插一套埋点逻辑。
这种思想在软件架构里叫 Event Sourcing,事件溯源:不只保存"现在的状态",还保存"状态是如何一步步变成现在这样的"。
它并不适合所有业务表。比如用户昵称的最终值,通常没必要为每次修改都建立复杂事件体系。但 Agent 的上下文、工具调用和流式输出天然是过程型数据,事件顺序本身就有价值。
DeepSeek Harness 还有一条很硬的纪律:任何能进入模型请求的内容,都必须能从会话日志重建。
这条纪律听起来严苛,实际是在避免一个很危险的问题:模型已经参考过一条内存里的"隐形上下文",但进程恢复后这条上下文消失了。此时用户看到的历史、模型接下来的判断和系统的审计记录会开始彼此矛盾。

08 工具执行管线:不是所有工具调用都该直接执行
想象模型发出两个请求。
第一个是 read_file,读取项目里的一个日志文件;第二个是 delete_file,删除一批文件。
如果工具定义只提供一个 execute() 函数,这两个动作看上去一样:参数到了,函数执行。
但从风险角度,它们完全不同。读取文件可能只需要路径规则检查;删除文件可能需要确认当前工作区、申请人工审批、记录审计事件,并确保超时或取消时不会留下半完成状态。
DeepSeek Harness 把这种治理放进统一的工具执行管线:
模型给出 tool call
-> 校验参数、解析工具
-> pre-execute:允许、要求审批,或拒绝
-> guards:执行不可绕过的策略检查
-> execute:运行真正的工具正文
-> post-execute:检查、替换或阻断结果
-> result:通知会话、UI 和其他观察者
pre-execute 和 post-execute 是两个很重要的扩展位置。
前者解决"现在能不能执行"。审批、权限策略、计划模式都可以在这里发言。
后者解决"这个结果能不能原样交回模型"。例如安全策略可以把不合规结果转成错误反馈;溢出存储策略可以把超长纯文本替换为首尾预览、存储定位信息和检索提示;更高层的 compaction 才可能进一步生成摘要。UI 则从最终规范化结果得到展示数据。
你可能会问:为什么不把这些规则写在每个工具里?
因为规则往往跨越多个工具。文件写入、Shell、终端、浏览器操作都可能需要审批。把审批写进每个工具,不只重复,而且很容易漏掉新工具。统一管线让策略一次注册,多个工具共享。
这里还有一个边界需要说清:统一管线不等于安全自动完成。真正的安全仍然依赖沙盒实现、文件策略、批准通道和部署配置。架构负责把这些责任放在可插拔、可审计的位置,而不是替代它们。

09 Capability Seam:如何把"可替换"做完整
"可以换模型"听起来像配置一个 API Base URL 就够了。
但真的想替换一项能力时,至少要回答三个问题:谁定义接口?谁提供实现?谁使用接口?
DeepSeek Harness 把这三种角色称为 Capability Seam,可以理解为"可替换能力的标准接头"。
- Service Definition :声明能力接口。比如文件系统需要
read、write、stat等什么操作。 - Service Provider:提供具体实现。它可以访问本机文件,也可以转发到 E2B 或受沙盒限制的环境。
- Consumer:真正使用这个接口的部分。对于文件系统,常见 Consumer 是面向模型的文件工具。
拿文件读取举例,Agent Loop 并不需要知道文件在本机、容器还是远程沙盒。它只关心"当前 Agent 可见的文件工具能否完成读取"。文件系统 Provider 换掉后,上面的工具和任务流程不用随之改写。
模型能力也是同样的结构:LLM 服务定义模型请求和流式响应的共同语言;DeepSeek、其他厂商或回放实现作为 Provider;Agent Loop 作为 Consumer 调用统一接口。
更有意思的是执行环境。Shell、子进程、终端、LSP 往往需要共享同一个"进程到底在哪里运行"的世界。如果每个工具各自实现一套远程逻辑,替换成本会非常高。
把底层 Provider 指向另一种执行环境后,上层的 Shell、PTY 终端、LSP 等能力就可以一起迁移。这才是接口分层真正节省复杂度的地方。

这一部分只记住三件事:
- Event Log 保存的不只是聊天内容,而是任务过程的可重建事实。
- 工具管线让跨工具的安全、审批和结果治理有一个统一入口。
- 真正的可替换能力必须同时拥有接口、实现和使用方,只有 Provider 不叫完整架构。
PART 04:插件化解决了什么,又把复杂度放到了哪里
10 这套架构真正得到的五个收益
第一,模型和执行环境可以独立替换。
当模型 Provider、文件系统 Provider、Shell Provider 都依赖统一服务接口时,替换不再意味着修改主循环。你需要验证的是新 Provider 是否满足接口和安全要求,而不是把整个 Agent 重写一遍。
第二,不同产品形态可以复用同一个运行时。
Web、命令行和自动化协议的交互方式不同,但它们可以围绕同一个 Session、Agent 和工具体系工作。差异留在组合层,而不是复制业务流程。
第三,策略有了集中治理的位置。
审批、超时、沙盒、持久化检查点、重试策略不必混入每一个工具和每一次模型调用。它们可以围绕事件和管线注册,并在需要时被替换或关闭。
第四,状态具备回放和恢复基础。
只要模型可见内容和关键过程都进入事件日志,页面、持久化、fork、转录和遥测就不必各自维护一份近似但不同的状态。
第五,扩展行为可以拥有明确的卸载边界。
这对热更新、动态配置和长期运行的服务尤其重要。一个插件被移除时,它注册的工具、监听器和提示词贡献也应该跟着消失。
11 它没有消灭复杂度,只是要求你诚实面对复杂度
插件化经常被误解成"拆开以后就简单了"。事实上,复杂度没有消失,它从一个大函数里移动到了接口、事件、配置和生命周期里。
这套架构至少有五类成本。
第一类是概念成本。 开发者要理解上下文、注入、作用域、事件模式和 disposer。尤其是 waterfall 事件,监听器如果没有正确调用 next(),就可能意外截断后续链路。
第二类是配置成本。 Profile 和 Patch 很灵活,但也要求你能回答"最终启动的插件树是什么"。配置覆盖越多,越需要可视化、校验和明确优先级。
第三类是边界成本。 什么时候该新增一个 Provider,什么时候只是一个普通函数?不是所有代码都值得插件化。没有独立变化需求、没有独立生命周期、没有多个 Consumer 的逻辑,过早拆分只会增加阅读负担。
第四类是状态纪律。 事件日志带来回放能力,也要求开发者不能偷偷把重要上下文留在某个内存变量里。这个约束很值得,但实现时需要持续坚持。
第五类是性能成本。 提示词和工具 schema 的动态组装要考虑稳定顺序、缓存和 token 开销;插件注册与观察也不能无限叠加。动态能力越强,越需要关注请求前缀和运行时边界。
因此,DeepSeek Harness 值得学习的不是"多建目录、多拆模块"。
它更像一份提醒:当系统复杂起来,别再假装所有行为都属于 Agent Loop。
12 给刚入门 Agent 架构的工程师:从哪里开始借鉴
你不需要一开始就做一套完整 Harness。
如果你的项目只有一个模型、两个只读工具、没有恢复需求,那么一个清晰的循环加几段函数就足够。为它提前引入复杂插件系统,反而会拖慢交付。
但当下面任意信号开始出现时,就应该考虑建立明确扩展点:
- 同一个任务要支持多个模型或多个部署环境;
- 工具开始区分只读、写入、高风险操作;
- 用户希望刷新后继续之前的任务;
- 你需要子 Agent、后台任务或多入口接入;
- 压缩、记忆、审批、重试开始改动同一个主循环;
- 新功能上线时,你发现自己总在修改那段最不敢动的
while循环。
最小的可借鉴版本,不需要很多抽象。你只要先做好四件事:
- 把模型、工具、会话定义成稳定接口,而不是到处直接调用具体实现。
- 为模型请求前和工具执行前后留出事件或中间件入口。
- 把"模型实际看到的历史"当成可重建状态,而不是临时数组。
- 只为真正会独立变化的能力建立 Provider,不为每个辅助函数创造插件。
当你能清楚回答"这项能力以后会不会换""它有没有自己的启动和销毁时机""是否会被多个模块使用"时,插件化就开始有价值。
反过来,如果一段代码只有一个调用方、永远跟着某个业务流程变化、拆开后仍要暴露大量内部细节,它更可能只是一个普通函数,而不是一种能力。
结尾:把变化请出核心循环
DeepSeek Harness 最值得理解的地方,不是某个模型适配器,也不是某个工具名字。
它提供了一种组织 Agent 系统的顺序:先用 Profile 决定装什么,用插件运行时管理生命周期,用 Agent Loop 推动最小任务流程,用事件日志保存共同事实,再用可替换能力接头承载模型、工具和执行环境的变化。
这条路不会让 Agent 系统天然变简单。
但它能让复杂度不再躲在一个越来越长、越来越不敢修改的循环里。
一个 Agent 能走多远,取决于模型;一个 Agent 系统能长多大,取决于新增能力时还需不需要反复打开它的心脏。
你现在的 Agent 项目里,权限、记忆、工具和重试是独立能力,还是还挤在同一个 while 循环里?