一切皆插件:DeepSeek Harness 如何重构 Agent 运行时

它想回答一个更实际的问题:当你的 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 变成不可替换的特权核心。

  1. 组合层决定"这次启动装哪些能力"。它由 Profile、Bundle 和配置覆盖组成。
  2. 插件运行时层负责加载插件、提供服务、派发事件和在卸载时清理注册项。
  3. 核心驱动层包含 Agent、Agent Loop、提示词组装、工具注册表、模型服务和会话。
  4. 能力层提供可替换实现,例如 DeepSeek 模型、文件系统、Shell、沙盒、审批、搜索、子 Agent、工作流。
  5. 状态与交互层把过程写入 Session Event Log,再投影到持久化、Web UI、CLI、自动化客户端和遥测。

如果用户在 Web 页面输入"帮我检查这个项目的测试失败原因",路径大致是这样的:

复制代码
用户输入
  -> Agent 收到待处理消息
  -> Agent Loop 组装系统提示词、可见工具和历史
  -> LLM 生成回答或 tool call
  -> 工具管线检查权限并执行
  -> 工具结果写入会话
  -> Agent Loop 决定是否继续下一步
  -> 会话事件投影到页面、持久化和其他观察者

这里最重要的一点是:模型请求和工具执行不是整套系统唯一的主角。

配置决定能力集合,事件决定谁能观察过程,日志决定系统重启后还能记住什么。把这三件事看见以后,你就能理解为什么一个成熟的 Agent 系统不能只靠一条聊天数组支撑。

03 插件运行时的五个基础概念

DeepSeek Harness 建在 Cordis 之上。第一次接触时,ServiceContextInjectEventEffect 这些名字很容易让人觉得抽象。我们逐个翻译成工程里的问题。

Service:我向系统提供什么能力?

例如"模型调用""工具注册""会话管理"都可以是服务。服务先声明一个稳定入口,比如 ctx.llmctx.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-stepagent/requestagent/request-errorllm/streamagent/turn-stopping 等事件上继续开放:

  1. 进入模型前:插件可以补充或拒绝本次输入,例如注入任务上下文、应用计划模式、触发压缩。
  2. 执行工具时:插件可以进行审批、沙盒限制、超时控制、结果加工和观测。

这就是"核心循环保持小"的真实含义。不是减少功能,而是让功能从循环外部通过明确的入口参与进来。

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-executepost-execute 是两个很重要的扩展位置。

前者解决"现在能不能执行"。审批、权限策略、计划模式都可以在这里发言。

后者解决"这个结果能不能原样交回模型"。例如安全策略可以把不合规结果转成错误反馈;溢出存储策略可以把超长纯文本替换为首尾预览、存储定位信息和检索提示;更高层的 compaction 才可能进一步生成摘要。UI 则从最终规范化结果得到展示数据。

你可能会问:为什么不把这些规则写在每个工具里?

因为规则往往跨越多个工具。文件写入、Shell、终端、浏览器操作都可能需要审批。把审批写进每个工具,不只重复,而且很容易漏掉新工具。统一管线让策略一次注册,多个工具共享。

这里还有一个边界需要说清:统一管线不等于安全自动完成。真正的安全仍然依赖沙盒实现、文件策略、批准通道和部署配置。架构负责把这些责任放在可插拔、可审计的位置,而不是替代它们。

09 Capability Seam:如何把"可替换"做完整

"可以换模型"听起来像配置一个 API Base URL 就够了。

但真的想替换一项能力时,至少要回答三个问题:谁定义接口?谁提供实现?谁使用接口?

DeepSeek Harness 把这三种角色称为 Capability Seam,可以理解为"可替换能力的标准接头"。

  • Service Definition :声明能力接口。比如文件系统需要 readwritestat 等什么操作。
  • 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 循环。

最小的可借鉴版本,不需要很多抽象。你只要先做好四件事:

  1. 把模型、工具、会话定义成稳定接口,而不是到处直接调用具体实现。
  2. 为模型请求前和工具执行前后留出事件或中间件入口。
  3. 把"模型实际看到的历史"当成可重建状态,而不是临时数组。
  4. 只为真正会独立变化的能力建立 Provider,不为每个辅助函数创造插件。

当你能清楚回答"这项能力以后会不会换""它有没有自己的启动和销毁时机""是否会被多个模块使用"时,插件化就开始有价值。

反过来,如果一段代码只有一个调用方、永远跟着某个业务流程变化、拆开后仍要暴露大量内部细节,它更可能只是一个普通函数,而不是一种能力。


结尾:把变化请出核心循环

DeepSeek Harness 最值得理解的地方,不是某个模型适配器,也不是某个工具名字。

它提供了一种组织 Agent 系统的顺序:先用 Profile 决定装什么,用插件运行时管理生命周期,用 Agent Loop 推动最小任务流程,用事件日志保存共同事实,再用可替换能力接头承载模型、工具和执行环境的变化。

这条路不会让 Agent 系统天然变简单。

但它能让复杂度不再躲在一个越来越长、越来越不敢修改的循环里。

一个 Agent 能走多远,取决于模型;一个 Agent 系统能长多大,取决于新增能力时还需不需要反复打开它的心脏。

你现在的 Agent 项目里,权限、记忆、工具和重试是独立能力,还是还挤在同一个 while 循环里?

相关推荐
长谷深风1111 小时前
ReAct框架:让AI像人类一样思考与行动
大数据·人工智能·热门·ai agent·agent工作流·智能体设计·ai产品设计
m4Rk_1 小时前
【论文阅读】Agent 记忆机制(43):Mem²Evolve——让经验与能力在双记忆中共同进化
论文阅读·人工智能·学习·开源·github
Eric.461 小时前
AI 漫剧量产实战:StableDiffusion 帧稳画算法 + ComfyUI 自动质检流水线搭建(附完整代码 + 配置)
人工智能·深度学习·stable diffusion·comfyui·ai漫剧
代码方舟1 小时前
企业级对公银行 KYC 架构:基于天远人脸身份证比对A构建自动化客户尽调网关
运维·人工智能·架构·自动化
xingyun862 小时前
DeepSeek涨价,你换了吗?——AI大模型服务成本与替代方案深度分析
人工智能
程序员cxuan2 小时前
DeepSeek Harness 必装的插件公布了!
人工智能·后端·程序员
Claire_882 小时前
自然语言处理在法律文书生成中的应用:以离婚协议为例的多工具功能对比
人工智能·自然语言处理
樊小肆2 小时前
DeepSeeker-Code源码导读05-计划模式与子Agent
人工智能·agent
办公室马主任2 小时前
乐图数字化打通哪几个环节?从车间数据到经营决策的数据链路设计
大数据·人工智能·系统架构·制造