网络协议实验室头像
DeepSeek Harness 源码级深度剖析:全插件架构 Agent 框架从入门到实战封面图

DeepSeek Harness 源码级深度剖析:全插件架构 Agent 框架从入门到实战

DeepSeek Harness 源码级深度剖析:全插件架构 Agent 框架从入门到实战

本文适合谁:正在构建 AI Agent 系统的架构师和高级开发者;对"插件化架构"感兴趣的后端工程师;想要深度理解 DeepSeek Harness(dsh)源码的 contributors。

你将获得:dsh v0.1.0-rc.8 架构概述与 5 个核心包(Session / Agent / Agent Loop / Tools / defineTool)的关键设计解析、Capability Seam 设计模式的完整剖析、一个可直接投产的 Test Guardian 测试守护插件(含跨文件依赖图、阻断式拦截、持久化缓存和 46 个测试用例),以及 10 条从架构中提炼的设计铁律。

这篇教程值在哪里?

如果你正在选型 Agent 框架——市面上的 LangChain、AutoGen、CrewAI 文档和教程已经泛滥,但 DeepSeek Harness 是唯一一个把"一切皆插件"贯彻到 Agent 循环本身的设计。理解了 dsh 的 Capability Seam 模式,你就掌握了评判任何 Agent 框架可扩展性的尺子。本文概述了 dsh 50+ 子包的架构全貌并深入解析了 5 个核心包的关键设计,帮你避开选型的认知盲区。

如果你正在构建 AI 编程工具——第九章的 Test Guardian 插件解决了一个行业级痛点:Agent 写完代码后测试自动跑、跑不过就阻断,不允许提交半成品。这个插件包含完整的跨文件依赖图(BFS 传递闭包)、阻断式 pre-execute 拦截、持久化缓存和三种语言适配器(Python/TypeScript/Go),共 1658 行代码、46 个测试用例,tsc --noEmit 零错误,可直接 clone 后投入生产。这不是教学示例——这是一个经过测试驱动开发(TDD)验证的、可直接解决真实问题的工业级插件。

如果你想学习插件化架构设计——dsh 的 Cordis 框架实现了"时空可组合性":插件可以运行时动态加载/卸载,卸载时自动回滚所有副作用。这种设计模式不仅适用于 Agent 框架,可以迁移到任何需要"热插拔"能力的系统中(IDE 插件、CI/CD pipeline、微服务网关)。本文从架构层面提炼了 10 条设计铁律,每一条都附带 dsh 源码中的设计依据。

教程的稀缺性——dsh 目前处于 developer preview 阶段,官方文档以 API reference 为主,缺乏架构级解读。本文是对 dsh packages/core/ 中 5 个核心包进行架构级深度解析的中文资料,并包含一个完整的实战插件实现。读完后,你不仅能深度理解 dsh 的架构设计,还能将 Capability Seam、事件溯源会话、waterfall 策略拦截等设计模式迁移到自己的项目中。

阅读建议:全文约 25000 字(含代码),建议分三段阅读——第一段(一至四章)理解框架骨架,第二段(五至八章)掌握运行时机制,第三段(九至十三章)动手实战。


前言:为什么值得关注 DeepSeek Harness?

2025 年,AI Agent 框架赛道已是一片红海——LangChain、AutoGen、CrewAI、OpenAI Codex SDK……每个都在尝试回答同一个问题:如何让 LLM 安全、可控、可扩展地使用工具?

DeepSeek Harness(简称 dsh)给出了一个与众不同的答案:Everything is a Plugin(一切皆插件)。

这不是一句营销口号。在 dsh 中,模型适配器是插件,工具注册表是插件,会话日志是插件,连 Agent 循环本身都是插件。这意味着你可以替换 Agent 的核心驱动逻辑,而不需要 fork 任何代码——只需写一个新插件注册到 ctx.agents。

这个架构选择带来了一个深远的好处:当你想要改变 Agent 的行为时,你不是在"配置"一个黑盒,而是在"组合"一组透明的、类型安全的、可逆的服务。

本文基于 v0.1.0-rc.8 版本源码,重点解析了 packages/core/ 下 5 个核心包(Session / Agent / Agent Loop / Tools / defineTool)的关键设计,并概述了 50+ 子包的架构全貌和实战指南。


一、项目全貌

1.1 一句话定位

dsh 是一个 Agent 运行时框架(agent harness),核心理念是"一切皆插件"。它基于 Cordis 插件框架(设计思想来自论文 A Programming Paradigm for Spatiotemporal Composability),用 TypeScript 6.0 strict 模式编写,运行在 Node.js ≥ 22.19 上。

1.2 技术栈一览

层级 技术选型 选型理由
运行时 Node.js ≥ 22.19(ESM only) 原生 ESM、AsyncLocalStorage、Fetch API
语言 TypeScript 6.0, strict: true 类型安全是框架的基石,不是锦上添花
包管理 pnpm 11.7 workspaces 50+ 子包的高效管理
插件框架 Cordis(vendored, SHA pinned) 时空可组合性,注册即副作用
配置验证 Schemastery 声明式 schema + 运行时验证
构建 tsdown(打包)/ tsc(类型) 分离构建与类型检查
测试 Vitest 4 单元/E2E/snapshot/web stress 全覆盖
沙箱 Landlock(Linux C11 原生模块) 内核级文件系统隔离
持久化 JSONL / SQLite 事件溯源 + 索引查询双通道
文档 VitePress 中英双语
Python SDK Python 3.10+, Pydantic, JSON-RPC over stdio 跨语言互操作

1.3 仓库结构

vendor/          # vendored Cordis 源码(SHA pinned,不可变)
packages/        # @deepseek-ai/dsh-<pkg> 工作空间
  core/          # 产品核心:session, system-prompt, tools, agent, agent-loop
  llm/           # LLM 适配:DeepSeek / pi-ai / replay
  shell/         # bash/pwsh 执行器
  fs/            # 文件系统操作
  subprocess/    # 子进程管理
  terminal/      # 持久终端
  web/           # Web 搜索/抓取
  sandbox/       # 进程沙箱
  subagent/      # 子代理(6种 provider)
  compaction/    # 上下文压缩
  session/       # 会话持久化
  ...            # 共 50+ 子包
apps/            # 产品组装层
  cli/           # dsh CLI
  web/           # Web UI
examples/        # 可运行的 cordis.yml 示例
native/          # Landlock C11 原生模块
python/          # Python SDK + 打包运行时
docs/            # 架构文档 + cookbook

关键洞察:vendor/ 目录中的 Cordis 源码是 SHA pinned 的——这意味着 dsh 对插件框架的依赖是可审计的、不可变的。不会因为上游发版而引入未知变更。这是企业级安全的基本要求。

在这里插入图片描述


二、Cordis 插件框架:五大核心理念

理解 dsh 的前提是理解 Cordis。这不是一个普通的依赖注入框架——它的设计哲学是时空可组合性:插件可以在运行时动态加载/卸载,且卸载时自动回滚所有副作用。

2.1 插件即 Service

插件是一个实现了 Service 接口的对象。两种写法等价:

// 写法 A:函数式插件(轻量场景)
export const name = 'my-plugin'
export const inject = ['tools', 'llm']
export function apply(ctx: Context) {
   
   
  ctx.tools.register(myTool)
}

// 写法 B:Service 子类(需要状态管理的场景)
class MyService extends Service {
   
   
  static inject = ['tools', 'llm']
  constructor(ctx: Context, config: Config) {
   
   
    super(ctx, 'myKey')  // 注册为 ctx.myKey
  }
}

2.2 Context 是服务仓库

服务通过 ctx.<key> 暴露自己。其他插件通过 key 查找服务,而非导入具体实现:

// 插件 A 注册服务(通过 ctx.plugin 加载 Service 子类)
ctx.plugin(MyService)

// 插件 B 消费服务(不知道具体实现)
ctx.myKey.doSomething()

这就是依赖倒置(DIP)在框架级别的实现:消费者依赖抽象的 key,不依赖具体的 import。

2.3 依赖声明通过 inject

export const inject = ['tools', 'llm']

Cordis 等待 tools 和 llm 服务就绪后才挂载该插件。加载顺序由服务依赖表达,无需手动编排。这解决了大型插件系统中常见的"循环依赖"和"加载顺序"问题。

2.4 类型化事件通信(五种模式)

这是 dsh 最精妙的设计之一。服务通过 TypeScript declaration merging 声明事件名,然后以五种模式分发(对应 Cordis DispatchMode 类型:'emit' | 'parallel' | 'serial' | 'bail' | 'waterfall'):

模式 是否 await 分发顺序 有返回值 典型用途
emit 否 注册序 否 通知(tools/result)
waterfall 否 注册序 是 策略拦截(tools/pre-execute),around-中间件模式
parallel 是 并行 否 批量通知(session/event)
serial 是 注册序 是 有序决策(agent/turn-stopping),返回值串联传递
bail 否 注册序 是 快速短路(第一个非空返回值即停止)

实战要点:waterfall 是 dsh 中策略拦截的核心机制——它实现了 around-中间件模式。监听器收到 (...args, next),调用 next() 委托给下一个监听器,不调用则短路。agent/pre-step、agent/request、llm/stream、tools/pre-execute、tools/execute、tools/post-execute 全部是 waterfall 事件。

2.5 注册即可逆副作用

所有注册(prompt section、tool schema、adapter、listener)都通过 ctx.effect() 或 ctx.on() 完成:

// 注册一个工具——卸载时自动注销
const dispose = ctx.effect(() => {
   
   
  ctx.tools.register(myTool)
  return () => ctx.tools.unregister(myTool)
})

// 或者更简洁的写法
ctx.on('tools/pre-execute', handler)
// 卸载时自动移除 listener

这意味着插件可以在运行时被安全卸载——所有副作用逆序回滚,不会留下僵尸注册。


三、核心包源码深度解析

3.1 Session —— 事件溯源的会话日志

源码:packages/core/session/src/index.ts

Session 是整个系统的真相源(source of truth)。它采用事件溯源(Event Sourcing)架构:

export class Session {
   
   
  private log: SessionEvent[] = []
  private readonly surfaceManager = new SurfaceManager(this.log)
  
  // 唯一写入路径:追加事件
  append<T extends SessionEventType>(
    type: T,
    data: SessionEventMap[T],
    ...opts: T extends SurfaceEventType ? [opts: SurfaceIntent] : []
  ): SessionEvent<T>
  
  // 从事件日志派生 LLM 消息历史
  deriveMessages(): Message[]
  
  // 请求头折叠(增量缓存)
  requestHeader(): EpochHeader | undefined
}

五个关键设计决策:

  1. 追加只写:事件一旦入日志即不可变(deepFreeze),seq 单调递增且连续(seq = log.length 契约)。这保证了会话的可审计性。

  2. Lossless JSON:所有事件数据经过 snapshotJsonValue() 递归验证,确保可序列化。BigInt、Symbol、Map/Set/Date 等异类对象在入口被拒绝。如果你试图把一个不可序列化的值塞进会话日志,它会在入口就被拦截,而不是在持久化时才报错。

  3. Surface 机制:消息产生事件(user/message、assistant/message、tool/result)携带 surfaceOp(append 或 replace)。SurfaceManager 维护一个有序节点列表,deriveMessages() 从中投影消息历史。压缩操作通过 replace 替换旧节点——这就是上下文压缩的实现方式。

  4. 版本控制:SESSION_FORMAT_VERSION = 0(未发布期,无兼容承诺)。结构变更 bump,新增事件类型不 bump(靠 ignorable 标记)。

  5. Fork:ctx.sessions.fork(source, boundary, childId) 从源会话的某个事件序号切出一个子会话。seed 是源日志的连续前缀,不允许在开放 turn 中间切分——这保证了 fork 的一致性。

事件类型完整分类:

// 持久会话事件(durable)——写入日志,可恢复
'turn/start'          // 开启一轮对话
'turn/end'            // 关闭一轮(携带 TurnEndReason)
'step/start'          // 开启一步(一次模型请求)
'step/end'            // 关闭一步
'user/message'        // 用户消息(含注入的上下文)
'assistant/chunk'     // 流式 Token
'assistant/message'   // 完整助手消息
'tool/call'           // 模型请求工具调用
'tool/result'         // 工具执行结果
'request/header'      // 请求配置快照
'request/context'     // 路由元数据
'session/end-seed'    // 种子结束标记
'todo/write'          // 待办列表快照

架构洞察:区分"持久事件"和"实时事件"是 dsh 的核心设计。持久事件(turn/*、step/*、user/message 等)写入日志,可从日志恢复。实时事件(agent/*、tools/*、llm/stream 等)是运行时通知,不持久化。这保证了会话恢复时只重建模型可见的状态,不重放运行时副作用。

3.2 Agent —— 代理注册表与发起者传播

源码:packages/core/agent/src/index.ts

AgentRegistry(ctx.agents)管理所有活跃 Agent 的生命周期:

export class AgentRegistry extends Service {
   
   
  private store = new Map<SessionId, AgentEntry>()
  private readonly initiators = new AsyncLocalStorage<Agent | undefined>()
  
  async create(options: CreateAgentOptions): Promise<AgentHandle>
  async resume(options: ResumeAgentOptions): Promise<AgentHandle>
  register(agent: Agent): () => void
}

三个关键设计:

  1. 工厂委托:AgentRegistry 不直接创建 Agent,而是委托给 AgentFactory(由 dsh-agent-loop 插件提供)。消费者通过 ctx.agents 编程,不依赖具体循环包。这意味着你可以替换 Agent 的驱动逻辑而不修改注册表代码。

  2. Initiator 传播:withInitiator(agent, operation) 使用 AsyncLocalStorage 将发起者 Agent 传播到异步调用链。子 Agent 创建时,设置窗口内的注册只对该 Agent 可见——这是 Agent 隔离的基础。

  3. Setup 窗口与回滚安全:CreateAgentOptions.setup 是创建时组合 Agent scoped world 的回调。工厂在 mint agentCtx 后、发布前 await setup。所有 scoped 注册在 agent/created 和第一个 prompt assembly 前完成。如果 setup throw/rejection,整个 scope 回滚,不发布 session 或 agent id——不会留下半初始化的 Agent。

3.3 Agent Loop —— 默认驱动器

源码:packages/core/agent-loop/src/agent.ts

ReactLoopAgent 是 Agent 接口的默认实现,驱动会话穿越 turn 和 step 边界:

export class ReactLoopAgent implements Agent {
   
   
  readonly inbox: Inbox
  private phase: Phase  // 'idle' | 'maintenance' | 'running'
  readonly scope: Scope
  readonly ctx: Context
  
  // 三种输入路径
  followup(input: UserMessage): void  // next-turn, 唤醒
  steer(input: UserMessage): void     // next-step, 唤醒
  inject(input: UserMessage): void     // next-step, 不唤醒
  
  private async turn(): Promise<boolean>  // 一轮对话
  private async step(assembly: PromptAssembly): Promise<StepEndReason | null>  // 一步
}

在这里插入图片描述

Turn/Step 流程深度解析:

  1. turn():开启 turn/start 事件 → 进入 step 循环 → 每个 step 调用 preStep() → 执行 step() → 判断是否继续 → 最终 turn/end。

  2. preStep():claim 输入消息 → assemble 系统提示词 → 运行 agent/pre-step waterfall(可 reject 或重写消息)。

  3. step():组装 LLM 请求 → 运行 agent/request waterfall → llm/stream waterfall → 逐 chunk 追加 assistant/chunk → 组装 assistant/message → 执行工具调用 → step/end。

  4. Inbox 三通道(这是 dsh 独特的设计):

    • next-turn:排队到下一轮(followup)
    • next-step:排队到当前轮的下一步(steer、inject)
    • inject 不唤醒 idle driver,只有 followup/steer 唤醒

    实战意义:inject 允许你在 Agent 运行时静默注入上下文,不打断当前流程。这在多 Agent 协作中非常有用——父 Agent 可以向子 Agent inject 上下文而不等待。

  5. Phase 状态机:

    idle → (wake) → running → (turn done) → idle
    idle → (maintenance) → maintenance → (done) → idle
    running → (abort) → running (aborted) → idle (latch wake)
    
  6. 请求头持久化:buildRequest() 在每个 step 记录 request/header(初始/resume/变更),确保模型可见的一切可从日志重建。

3.4 Tools —— 工具注册表与执行管道

源码:packages/core/tools/src/index.ts

ToolRuntime(ctx.tools)是整个工具系统的核心——注册、查找、执行、策略拦截的统一入口。

在这里插入图片描述

管道十阶段(有序执行,每个阶段都是可拦截的):

阶段 事件 类型 能力
1 tool/call 日志 模型请求工具调用,记录到会话日志
2 presentCall(args) 纯函数 UI 渲染 pending 卡片(可用于 replay)
3 tools/pre-execute waterfall 策略层:返回 allow/deny/ask
4 Monotonic Guards 同步检查 不可逆拒绝——一旦 deny,后续无法放行
5 tools/execute waterfall 可替换 exec.signal(施加超时),信号融合
6 execute() 工具主体 返回 canonical JSON value
7 tools/post-execute waterfall accept/block/replace/addContext
8 normalizeResult() 不变式 快照 → JSON 验证 → 冻结
9 tools/result emit 冻结的不可变最终结果通知
10 tool/result 日志 持久化到会话日志

核心价值点:理解这十个阶段是开发 dsh 插件的关键。特别是第 3 阶段(pre-execute)和第 7 阶段(post-execute)——它们是自定义安全策略和结果增强的挂载点。后面的 Test Guardian 插件就是利用第 3 阶段实现阻断式测试验证的。

三个高级设计:

  1. Code Mode:mode: 'code' 时,模型只能直接调用 run_code,其他工具通过程序内 await tools.xxx(args) 调用。子调用携带 parent token,记录 tool/code-dispatch 事件,遵守原生调度契约。这让 Agent 可以编写程序化的工具调用逻辑,而不是一次一个工具调用。

  2. Scoped Shadowing:通过 agent.ctx 注册的工具会 shadow 同名全局工具。restrict() 可对全局工具集做 allow/deny 过滤。你可以为每个 Agent 配置不同的工具集。

  3. 并发安全:工具声明 isConcurrencySafe(args) 返回 true 时,可加入 parallel group。否则为 exclusive(排序屏障)。

3.5 defineTool —— 类型安全的工具定义

源码:packages/core/tools/src/schema.ts

defineTool 是一等工具的推荐定义方式:

export function defineTool<const S extends ParameterSchemaSpec, const O extends ValueSchemaSpec>(
  options: DefineToolOptions<S, O>
): ToolDefinition

它做了六件事:

  1. 将作者友好的 schema DSL 编译为 raw JSON Schema
  2. 编译过程栈安全(迭代式,非递归),检测循环引用
  3. InferArgs<S> 从 schema 推断 TypeScript 参数类型
  4. InferValue<O> 从 output schema 推断返回值类型
  5. execute 的参数自动验证(validateArgs 在 execute 前调用)
  6. presentCall/presentResult 软验证(replay 时不 throw,回退到 generic)

实战示例:

import {
   
    readFile } from 'node:fs/promises'

const readFileTool = defineTool({
   
   
  name: 'read_file',
  description: '读取文件内容',
  parameters: {
   
   
    path: {
   
    type: 'string', description: '文件路径', required: true },
    encoding: {
   
    type: 'string', default: 'utf-8' },
  },
  output: {
   
   
    type: 'object',
    properties: {
   
   
      content: {
   
    type: 'string' },
      size: {
   
    type: 'number' },
    },
  },
  async execute(args) {
   
   
    // args 类型由 InferArgs 自动推断
    // args.path: string (required)
    // args.encoding: string (default 'utf-8')
    const content = await readFile(args.path, args.encoding as BufferEncoding)
    return {
   
    content, size: content.length }
  },
})

注意:args.encoding 的类型是 string,而 readFile 的第二个参数类型是 BufferEncoding('utf-8' | 'ascii' | ...)。在 strict 模式下需要显式断言 as BufferEncoding。这是 TypeScript strict 模式下类型安全的正确做法——不在框架边界做隐式转换。


四、Capability Seam 设计模式

这是 dsh 最核心的架构模式,也是它区别于其他 Agent 框架的关键。

4.1 三角色分离

一个 Capability Seam 由三个独立角色组成:

角色 职责 示例
Service Definition 声明接口和 ctx.<key> dsh-shell(ShellExecutor 抽象类)
Service Provider 实现接口 dsh-bash-local / dsh-bash-sandbox
Consumer 使用服务 dsh-tool-bash(模型可见的 bash 工具)

为什么三角色分离是关键:当一个 provider swap 时,整个产品行为改变。例如,将 ctx.fs 从 fs-local 换成 fs-e2b(远程沙箱),Bash、PTY、LSP 都跟着迁移到远程 Linux 运行时,零 provider fork。

这不是理论——这是 dsh 的实际能力。你可以用同一套 Consumer 代码,在本地、Docker 沙箱、E2B 远程沙箱之间无缝切换。

4.2 完整的 Capability Seam 列表

ctx key 角色 实现包
ctx.llm seam llm-deepseek / llm-pi-ai / llm-replay
ctx.fs seam fs-local / fs-sandbox / fs-e2b
ctx.shell seam bash-local / bash-sandbox / pwsh-local
ctx.subprocess seam subprocess-local / subprocess-e2b
ctx.terminals seam terminal-bash
ctx.sandbox seam sandbox-local
ctx.web seam web-search-exa / web-search-perplexity / web-search-deepseek / web-fetch-http
ctx.compaction seam compaction-basic
ctx.subagents seam spawn-in-process / fork-in-process / acp / codex / claude-code / dsh-sdk
ctx.approval seam acp
ctx.codeRuntime seam code-runtime-worker
ctx.lsp seam lsp-local
ctx.skills seam skill-badge / skill-filesystem
ctx.jobs seam jobs-local
ctx.sessionPersistence seam session-persistence-jsonl / session-persistence-sqlite
ctx.storage seam storage-json / storage-sqlite
ctx.workflowEngine seam workflow-worker-thread
ctx.spillStore seam spill-local

核心价值点:这张表是 dsh 架构的"地图"。当你想要扩展某个能力时,先找到对应的 seam,然后实现 Service Provider 接口。不需要修改 Consumer 代码。


五、Profile 和 Bundle 组合机制

dsh 的运行时是一个从启动时组合的插件树。

  • Profile:存储在 Harness home 中的命名组合。列出它堆叠的 bundle、持有的 out-of-tree 插件、用户的 cordis.patch.yml。web 和 headless 作为模板随产品分发。
  • Bundle:Cordis 配置行和其所挂载代码的分发格式。每个在 package.json 的 dsh 字段声明自己。

组合顺序(从空到满):

  1. Profile 中各 bundle 的声明顺序
  2. Profile 的 cordis.patch.yml
  3. Home 级 cordis.patch.yml
  4. --patch 覆盖
# 查看实际启动的插件树
dsh --profile web --dump-config

dsh-base 是每个 profile 的第一层:模型适配器、工具、持久化、沙箱和批准策略、设置、凭据、遥测。dsh-web-app 添加浏览器应用;dsh-headless 添加无服务器的单次运行器。

实战技巧:在开发自定义插件时,先创建一个 cordis.patch.yml 来覆盖默认配置,而不是修改 bundle 源码。这样你的定制是可逆的、可追踪的。


六、持久化与可恢复性

6.1 JSONL 持久化

dsh-session-persistence-jsonl 将会话事件流式写入 .jsonl 文件(每行一个 JSON 事件)。支持 zstd 压缩。文件存储在 root(默认 .sessions)目录下。

适用场景:开发环境、小规模部署。每个会话一个文件,便于调试和查看。

6.2 SQLite 持久化

dsh-session-persistence-sqlite 使用 SQLite 的单调 SCHEMA_VERSION 持久化会话事件。支持全文本搜索和过滤。

适用场景:生产环境。索引查询能力强,支持会话搜索和批量管理。

6.3 恢复流程

AgentRegistry.resume(options)
  → ctx.sessionPersistence.prepare
    → 加载事件
    → 构造 Session.fromRestore()
    → mint agentCtx
    → await setup
    → 发布
    → 启动循环

恢复是完整的——不仅恢复会话日志,还重建 Agent 的 scoped world(工具、prompt section、listener)。这意味着一个被恢复的 Agent 可以立即继续工作,行为与被中断前完全一致。


七、Python SDK

dsh 的 Node.js 侧通过 @deepseek-ai/dsh-cordis-client-runner 提供 JSON-RPC over stdio 接口。Python 侧通过 deepseek-harness PyPI 包提供协议感知的 API 客户端。

注意:deepseek-harness Python 包(v0.2.0)是一个 DeepSeek V4 API 客户端封装,提供协议层面的安全防护(tool call salvage、reasoning content 保留、cache 字段规范化),不是 dsh Agent 框架的 Python 绑定。如需通过 Python 调用 dsh Agent 功能,需要直接使用 dsh --profile headless CLI 并解析输出。

7.1 DeepSeekHarness — DeepSeek V4 协议感知客户端

from deepseek_harness import DeepSeekHarness

# DeepSeekHarness 是 OpenAI SDK 的协议安全封装
# 构造函数签名:(api_key, base_url, *, salvage_tool_calls, normalize_cache_fields, ...)
harness = DeepSeekHarness(
    api_key="your-key",
    # base_url 默认 https://api.deepseek.com
    disable_thinking_by_default=False,  # 成本敏感部署可设 True
)

# 接口与 OpenAI SDK 完全一致:
response = harness.chat.completions.create(
    model="deepseek-v4-flash",
    messages=[{
   
   "role": "user", "content": "解释这段代码"}],
)

print(response.choices[0].message.content)

DeepSeekHarness 在每次请求中自动执行三层安全防护:

  • from_deepseek_response:保留 reasoning_content 字段
  • salvage_tool_calls_from_content:修复约 11% 的 tool call 泄漏(工具调用被错误地放在 content 而非 tool_calls 中)
  • normalize_usage:规范化两种 cache-hit 字段格式并补充成本估算

7.2 高级功能

from deepseek_harness import (
    DeepSeekHarness,
    normalize_usage,
    estimate_cache_hit,
    ReasoningLifecycle,
    salvage_tool_calls_from_content,
)

# 流式请求——ReasoningLifecycle 管理 thinking 块
harness = DeepSeekHarness(api_key="your-key")
stream = harness.chat.completions.create(
    model="deepseek-v4-pro",
    messages=[{
   
   "role": "user", "content": "分析这段代码"}],
    stream=True,
)
for chunk in stream:
    print(chunk.choices[0].delta.content, end="")

核心设计:

  • 协议安全:自动修复 DeepSeek V4 已知的 11 种协议异常(tool call 泄漏、空终块、cache 字段不一致等)
  • 成本控制:disable_thinking_by_default=True 可在简单请求中禁用 reasoning token(默认 V4-Pro 会对每个请求产生约 30 reasoning tokens 的费用)
  • 异常类型:HarnessError、ReasoningContentMissingError、ToolCallLeakageError、StrictModeCorruptionError、StreamShapeError

八、Landlock 沙箱

@deepseek-ai/node-addon-landlock-run 是一个 Linux 原生 C11 程序(约 300 行),使用 Landlock 内核 UAPI 实现自限制后执行:

  1. 在自身安装 Landlock ruleset
  2. execve 被包装的命令
  3. ruleset 跨 execve 继承,因此命令和所有子进程都被限制
  4. 调用进程不受限制
  5. Fail-closed:内核无法执行时,不运行命令直接退出
import {
   
    grantArgs, launcherPath, probe } from '@deepseek-ai/node-addon-landlock-run';

const launcher = launcherPath();
if (probe(launcher) !== 'unusable') {
   
   
  const argv = [
    launcher,
    ...grantArgs({
   
    readOnly: ['/'], readWrite: ['/tmp/work'] }),
    '--', 'bash', '-c', command
  ];
  // spawn argv
}

支持 linux-x64 和 linux-arm64,内核 5.13+。其他平台使用不同的限制后端(macOS 用 sandbox-exec,Windows 用 ACL restricted-token)。

安全洞察:Landlock 的关键优势是子进程继承限制。即使 Agent 通过 shell 启动了一个子进程,子进程也受同样的文件系统限制。这比应用级的权限检查更可靠——它是内核级的。


九、实战:Test Guardian 测试守护 Agent

这是本文的核心实战部分。我们将编写一个高价值插件,解决 AI Agent 编程中的真实痛点:Agent 修改代码后,如何确保不引入回归?

为什么这个插件值钱? 在 AI 编程工具(Cursor、Copilot、Devin、dsh)的实践中,Agent 生成的代码约 15-30% 存在隐性回归——单元测试不通过、导入路径断裂、接口签名不匹配。现有方案依赖人工 review 或 post-hoc CI,反馈周期长、成本高。Test Guardian 将测试验证前置到代码写入的瞬间:Agent 写代码 → 自动跑受影响测试 → 不通过就阻断,不允许坏代码落盘。这个插件可以直接部署到任何使用 dsh 的生产环境中,将 AI 编程的代码质量从"事后兜底"升级为"事前拦截"。

9.1 问题分析:为什么不是"代码审查"?

初版方案是一个基于 tools/result 事件监听的代码审查插件。它存在四个根本局限:

局限 原因 后果
单文件视角 只审查被修改文件本身的 diff 无法发现跨文件的回归
非阻断式反馈 使用 tools/result(emit 事件),操作已经完成 Agent 可能忽略反馈直接结束
无缓存 每次重新分析全部文件 重复工作,性能浪费
审查主观性 LLM 审查是主观判断 "审查深度不够"是固有问题

Test Guardian 的重新设计:

局限 解决方案 机制
单文件视角 → 解决 双向 import 依赖图 + BFS 传递闭包 DependencyGraphManager
非阻断式 → 解决 改用 tools/pre-execute waterfall 返回 { verdict: 'deny' } 短路
无缓存 → 解决 依赖图 + 文件哈希持久化到 ctx.storage 增量更新,重启恢复
审查主观性 → 解决 改为测试验证而非代码审查 通过/失败是客观二元判定

核心思想:不问"这段代码好不好",只问"这段代码的测试通过了吗"——从主观判断升级为客观验证。

9.2 插件结构

packages/extensions/test-guardian/
  package.json
  tsconfig.json
  vitest.config.ts
  src/
    index.ts              # 插件入口:注册 pre-execute 拦截 + result 依赖更新
    types.ts              # 类型定义:PreExecuteVerdict / LanguageAdapter / TestResult / ToolExec / Config
    vendor.d.ts           # 外部依赖类型桩:@deepseek-ai/cordis / schemastery(无完整 dsh 时编译用)
    dependency-graph.ts   # 依赖图管理器:双向邻接表 + BFS 传递闭包
    test-runner.ts        # 测试运行器:防递归 + 多语言测试执行
    sandbox.js            # 动态沙箱兼容版(纯 JS,可直接粘贴进 cordis_define)
    adapters/
      python.ts           # Python 适配器:pytest / from...import
      typescript.ts       # TS/JS 适配器:vitest / import...from
      go.ts               # Go 适配器:go test / import
  tests/
    test-guardian.spec.ts # 完整测试套件:46 用例
    e2e-verification.ts   # 端到端实战验证脚本:6 场景

9.3 核心架构

 Agent 调用 write/edit 工具
       ↓
  tools/pre-execute waterfall 触发     ← 阻断式拦截点
       ↓
  TestGuardian 检查文件类型
       ↓
  更新依赖图(增量,基于文件哈希)
       ↓
  查依赖图:哪些文件受影响?             ← 跨文件影响分析
  (BFS 传递闭包:reverse 图遍历)
       ↓
  ┌─ 有测试文件 ─→ 运行测试 ─────────────┐
  │   (ctx.shell 执行,防递归保护)       ├─ 通过 → allow(放行)
  │                                       └─ 失败 → deny(阻断)+ inject 反馈
  └─ 无测试文件 ─→ LLM 生成测试骨架 ─→ 运行 ─┐
                                              ├─ 通过 → allow
                                              └─ 失败 → deny + 反馈
       ↓
  tools/result 事件同步依赖图              ← 持续更新依赖图
  (即使 pre-execute 被跳过,也维护图)

9.4 类型定义(types.ts)

/**
 * pre-execute waterfall 的返回值。
 * dsh 源码中定义了三种裁决:
 * - 'allow':放行,继续执行工具
 * - 'deny':短路,拒绝执行,reason 返给模型
 * - 'ask':请求人工批准(通过 ctx.approval)
 */
export type PreExecuteVerdict =
  | {
   
    verdict: 'allow' }
  | {
   
    verdict: 'deny'; reason: string }
  | {
   
    verdict: 'ask'; reason: string }

/**
 * 工具执行请求(exec)的描述。
 * 这是在 pre-execute waterfall 中接收到的参数。
 */
export interface ToolExec {
   
   
  /** 工具名称,如 'write'、'edit'、'bash' */
  name: string
  /** 工具参数(已经过 validateArgs 验证) */
  args: Record<string, unknown>
  /** 发起此工具调用的 Agent(可能为 undefined) */
  agent?: {
   
   
    inject: (message: unknown) => void
  }
  /** 取消信号 */
  signal?: AbortSignal
}

/**
 * 语言适配器接口(ISP 原则:消费者只依赖需要的方法)。
 * 每种编程语言实现此接口,提供测试发现、import 解析、测试执行能力。
 */
export interface LanguageAdapter {
   
   
  readonly language: string
  readonly extensions: readonly string[]
  matches(filePath: string): boolean
  parseImports(content: string, ownPath: string): string[]
  findTestFile(sourcePath: string): string | null
  buildTestCommand(testFiles: string[], projectRoot: string): string
  

转载自 CSDN-专业IT技术社区

原文链接:https://blog.csdn.net/johnsong2009/article/details/163969204

文章来源转载

评论

评论列表

微信小程序
QQ小程序

关于作者

点赞数:0
关注数:0
粉丝:0
文章:0
关注标签:0
加入于:--