DeepSeek Harness:PTC 模式是如何用 TypeScript 编排工具调用的

PTC 到底是什么

DeepSeek Harness 的 PTC 模式,官方英文页面称为 Code Mode。PTC 通常被解释为 Programmatic Tool Calling,意思是“通过程序调用工具”。

它不是让模型帮你写一个需要保存的 TypeScript 文件,而是让模型临时生成一段 TypeScript 程序,用这段程序编排多个工具调用,然后由 Harness 执行它。

可以把它概括成一句话:

普通模式是“模型调用工具”;PTC 模式是“模型写一个小程序,而小程序调用工具”。

模型仍然负责理解需求、规划步骤和生成代码;TypeScript 则负责循环、分支、并发、错误捕获以及中间数据处理。

四种 mode 分别做什么

截图中的四个 mode,本质上是四套不同的 Agent preset。它们不是四个不同的模型,而是决定当前 Agent 能看到哪些工具、怎样调用工具,以及是否可以检查和改造运行时。

Mode 核心能力 适合场景
Standard mode 完整工具集,直接调用工具 日常编码和一般 Agent 任务
PTC mode 通过 TypeScript 程序编排工具 批量、重复和多步骤任务
Minimal mode 持久 Shell 加文件编辑 简单编码、基准测试和最小环境
Creator mode 检查运行时并创建自定义 preset 开发和实验 Agent 配置

Standard mode:功能完整的默认工作模式

Standard mode 可以看作普通的全功能编码 Agent。它通常直接向模型提供文件编辑、Shell、文件搜索、网页搜索、Skills、计划、目标、子 Agent 和工作流等能力。

例如用户说:

查看项目结构,修改登录页面,然后运行测试。

Standard mode 会让模型按步骤直接调用搜索、编辑和 Shell 工具:

模型 → 搜索文件
工具 → 返回结果
模型 → 读取文件
工具 → 返回内容
模型 → 编辑文件
工具 → 返回结果
模型 → 运行测试
工具 → 返回结果

它的优点是直观、可观察,每一次工具调用都清楚地展示出来。读取少量文件、修改一个功能或执行几个明确步骤时,Standard mode 往往是最容易理解和排错的选择。

PTC mode:让程序编排工具

PTC mode 就是本文重点讨论的模式。它把工具调用入口收敛为 run_code,然后向模型提供一个 TypeScript SDK。模型可以在一段程序里循环调用工具、并行执行独立任务、捕获错误并整理结果。

例如批量检查 50 个文件时,模型可以生成一个程序,在程序内部完成读取和过滤,最后只返回问题列表,而不必把每个文件的完整内容都逐次送回模型。

因此,PTC mode 更像是:

模型负责设计程序
↓
run_code 执行程序
↓
程序调用多个工具
↓
程序过滤、聚合结果
↓
模型读取最终结果

它尤其适合批处理和长工作流,但如果只是调用一个工具,使用 PTC 的编排优势就不明显了。PTC 的详细机制见下文。

Minimal mode:最小化的编码 Agent

Minimal mode 只保留两类核心能力:持久 Bash 和文件编辑器。官方页面将它描述为一个使用 persistent bash 与 str_replace_editor 的双工具编码 Agent。

它不是功能更强的模式,而是刻意减少能力后的最小运行环境。这样做有两个价值:

  • 工具越少,模型面对的选择越少,行为更容易分析。
  • 不同模型可以在相近的工具条件下进行编码能力测试。

可以把它理解成一个非常精简的工作台:模型通过 Shell 查看和操作环境,通过编辑器修改文件,没有完整模式中的搜索、计划、子 Agent 和工作流等额外能力。

适合的场景包括:

  • 测试模型最基本的读文件、改文件和运行命令能力
  • 调试一个 Agent 是否依赖某个高级工具
  • 在最小权限和最小工具集下运行简单编码任务

Creator mode:创建和实验自定义 Agent

Creator mode 面向的是 Harness 开发者,而不是普通使用者。它包含 Standard mode 的完整能力,并额外提供运行时检查、Cordis 插件实验和 preset 创作指导。

这里的 preset,可以理解为一套 Agent 配置:它规定使用哪些插件、提供哪些工具、采用哪一种工具呈现方式,以及如何组织会话、工作流和运行策略。

Creator mode 的典型流程是:

  1. 检查当前 Harness 已加载的运行时和插件。
  2. 在内存中试验某个 Cordis 插件。
  3. 组合需要的模型、工具、Skills 和执行能力。
  4. 创建一个新的 Agent preset。
  5. 用新的 preset 运行和验证任务。

所以,Creator mode 不是“回答质量更高的模式”,而是“用来制造其他模式的开发模式”。如果只是想让 Agent 帮你写代码,通常不需要选它;如果想研究插件组合或定制自己的 Agent,它才是合适的入口。

如何选择

可以用下面的简单规则:

日常写代码、改项目       → Standard mode
批量处理、多步编排 → PTC mode
测试最小工具集 → Minimal mode
开发插件、制作 preset → Creator mode

四种 mode 也可以按“抽象程度”来理解:

Minimal     只给模型最基本的工具
↓
Standard 给模型完整的现成能力
↓
PTC 让模型用程序组合这些能力
↓
Creator 让开发者重新组合并创建这些能力

这四者不是简单的“低级到高级”关系。PTC 并不一定比 Standard 更强,Minimal 也不只是 Standard 的残缺版本;它们针对的是不同的控制方式、调试需求和使用场景。

Cordis 是什么

Cordis 是 DeepSeek Harness 底层使用的 TypeScript 插件框架。可以把它理解为 Harness 的“插件运行时”或“操作系统内核”。

DeepSeek Harness 负责提供 Agent 能力,而 Cordis 负责把这些能力加载、连接、管理和卸载起来。模型、工具、文件访问、Agent loop 等能力,都可以作为插件挂载到同一个共享上下文中。

可以用下面的关系来理解:

Cordis       = 插件运行时 / 内核
DeepSeek = 模型
Harness = Agent 应用
工具插件 = 文件、Shell、网页、MCP 等能力

这也是 DeepSeek Harness 所说的“一切皆插件”:模型、工具、Skills、会话、沙箱、存储、循环、调度器和 UI,都可以被替换或重新组合,而不必把逻辑硬编码在 Harness 核心里。

Context:插件共享的上下文

Cordis 用 Context 作为插件之间共享服务的容器。概念上,运行中的 Harness 可能通过上下文暴露这些服务:

ctx.llm       // 模型服务
ctx.tools // 工具注册表
ctx.sessions // 会话管理
ctx.logger // 日志服务

插件不需要直接导入另一个插件的具体实现,而是通过 ctx 获取它需要的服务。这样可以替换服务的实现,同时保持插件接口不变。

Plugin:独立的功能模块

一个最简单的 Cordis 插件可以这样写:

export const name = "hello"

export function apply(ctx) {
console.log("hello from my first plugin")
}

然后在配置文件中加载它:

- name: "./hello.ts"

Cordis 加载配置后,会调用插件的 apply(ctx) 函数。插件可以在这里注册服务、监听事件或增加工具。

Service:插件提供的服务

服务是插件对外提供的长期能力,例如 ctx.tools、ctx.llm 和 ctx.sessions。插件可以通过 inject 声明依赖:

const plugin = Object.assign(
function apply(ctx) {
ctx.logger.info("plugin started")
},
{
inject: ["logger"]
}
)

这表示该插件需要 logger 服务,Cordis 会等依赖准备完成后再启动它。因此,插件的启动顺序由依赖关系决定,而不是简单依赖配置文件中的排列顺序。

Event:插件之间的通信

插件还可以通过事件进行通信,而不需要互相直接调用:

export function apply(ctx) {
ctx.on("app/ready", () => {
console.log("Application is ready")
})
}

当其他插件触发 app/ready 时,这个监听器就会执行:

ctx.emit("app/ready")

这种方式可以降低插件之间的耦合,让多个独立能力通过事件协作。

Lifecycle:插件的生命周期

Cordis 不只是负责加载插件,也负责管理插件的生命周期:

加载 → 启动 → 运行 → 停止 → 清理副作用

例如,一个插件可能注册事件监听器、定时器或上下文修改。当插件被卸载时,相关资源也应该被清理,避免监听器和定时任务继续残留。

“空间组合”和“时间组合”

Cordis 背后的设计论文使用了 spatiotemporal composability(时空可组合性)这个概念。这里的“空间”和“时间”指的是软件组件的两个维度。

空间可组合性,指插件可以声明彼此的依赖关系:

Agent Loop
↓ 依赖
Tool Registry
↓ 依赖
MCP Server

Cordis 根据这些依赖决定哪些插件应该激活,以及何时激活。

时间可组合性,指插件被卸载后,它产生的副作用也应当能够被撤销:

加载插件
→ 注册服务
→ 注册事件
→ 添加定时器

卸载插件
→ 移除服务
→ 移除事件
→ 清理定时器

这让插件可以动态加入和退出,而不是只能在程序启动时一次性加载。

Cordis 与 DeepSeek Harness 的关系

DeepSeek 模型
↓
DeepSeek Harness
↓
Agent Loop、工具、会话、沙箱、工作流、UI
↓
Cordis
↓
Context、Service、Event、Plugin Lifecycle

更简单地说:

Harness 定义 Agent 能做什么;Cordis 定义这些能力如何被装配和运行。

PTC 模式也建立在这个插件化思路上:run_code、工具注册表、代码运行时和工具调度器,都是可以被 Cordis 管理和组合的能力模块。

普通工具调用的问题

假设用户提出这样的请求:

找出项目里所有 TODO,读取相关文件,然后给我一个汇总。

在普通模式中,过程大致是:

模型 → glob("**/*.ts")
工具 → 返回文件列表

模型 → read_file("a.ts")
工具 → 返回文件内容

模型 → read_file("b.ts")
工具 → 返回文件内容

模型 → grep("TODO")
工具 → 返回结果

模型 → 总结

每次调用工具,都需要经过一次“模型决定 → 工具执行 → 结果回到模型”的循环。文件数量一多,模型往返次数和上下文中的中间结果都会增加。

PTC 如何改变调用流程

在 PTC 模式下,模型主要看到一个特殊工具:

run_code

Harness 同时会在系统提示词中提供一份 TypeScript SDK 声明,告诉模型当前有哪些工具、每个工具需要什么参数以及会返回什么类型的数据。

于是模型可能生成这样的程序:

const files = await tools.glob({
pattern: "**/*.ts"
})

const results = await Promise.all(
files.map(file =>
tools.read_file({
path: file,
limit: 10000
})
)
)

const todos = results.flatMap(result =>
result.content
.split("\\n")
.filter(line => line.includes("TODO"))
)

return {
filesChecked: files.length,
todos
}

模型实际发给 Harness 的,是一次外层调用:

{
"description": "Find TODO comments across TypeScript files",
"code": "const files = await tools.glob(...); ..."
}

Harness 收到这段代码后,会启动代码运行时,执行 TypeScript,并把程序里的 tools.glob()、tools.read_file() 映射为真正的工具调用。最后,只有程序的 return 值和 console.log() 输出会作为结果回到模型。

tools.xxx() 是什么

tools.xxx() 不是普通的 JavaScript 函数,而是 Harness 注入给程序的异步工具绑定。例如:

await tools.bash({
command: "git status --short",
description: "Check git status"
})
await tools.read_file({
path: "/project/package.json"
})

概念上,Harness 会为模型提供类似下面的类型声明:

declare const tools: {
glob(args: {
pattern: string
}): Promise<string[]>

read_file(args: {
path: string
limit?: number
}): Promise<{
content: string
}>

bash(args: {
command: string
description: string
}): Promise<{
stdout: string
exitCode: number
}>
}

这让模型能够按照工具的参数和返回类型来写程序。需要注意的是:这些声明只在 run_code 程序内部有效。在 PTC 模式下,模型不能直接调用 read_file,只能在 run_code 中间接调用它。

一次 PTC 调用的执行过程

一次完整的 PTC 调用可以拆成下面几步:

  1. 模型理解用户目标,决定需要哪些工具。
  2. 模型生成一段 TypeScript 程序。
  3. Harness 把程序交给代码运行时执行。
  4. 程序通过 tools.xxx(args) 调用实际工具。
  5. 每个内部工具调用仍会经过权限、策略、执行和审计流程。
  6. 程序在本地过滤、合并和压缩中间结果。
  7. return 或 console.log() 的内容回到模型。

因此,PTC 不是绕过工具系统,而是增加了一层“程序化编排”。

为什么 PTC 有用

减少模型往返

普通模式可能是:

调用 A → 回模型
调用 B → 回模型
调用 C → 回模型
调用 D → 回模型

PTC 可以变成:

模型生成程序 → 一次执行 A、B、C、D → 回模型

这对批量读取文件、批量查询 API、检查多个目录或处理多条数据尤其有用。

在工具内部处理数据

普通模式下,读取 100 个文件后,100 份内容可能都要进入模型上下文;PTC 可以先在程序中筛选:

const files = await tools.glob({
pattern: "**/*.ts"
})

const matches = []

for (const file of files) {
const result = await tools.read_file({
path: file
})

if (result.content.includes("TODO")) {
matches.push({
file,
count: result.content.split("TODO").length - 1
})
}
}

return matches

文件全文只在这段程序内部流转,模型最终只看到 matches。这能减少上下文噪声和不必要的 token 消耗。

支持循环、分支和错误处理

模型不必每遇到一个失败就重新规划一次。程序可以捕获错误并继续处理:

const urls = [
"https://example.com/a",
"https://example.com/b",
"https://example.com/c"
]

const results = []

for (const url of urls) {
try {
const page = await tools.fetch_url({ url })

results.push({
url,
ok: true,
title: page.title
})
} catch (error) {
results.push({
url,
ok: false
})
}
}

return results

可以并行调用

彼此独立的只读任务可以使用 Promise.all():

const [a, b, c] = await Promise.all([
tools.read_file({ path: "src/a.ts" }),
tools.read_file({ path: "src/b.ts" }),
tools.read_file({ path: "src/c.ts" })
])

return {
a: a.content.length,
b: b.content.length,
c: c.content.length
}

但写文件、提交代码和删除文件等有副作用的操作通常应该串行执行。Harness 会依据工具的并发安全属性进行调度;当前配置中的 maxParallelSubCalls 默认值为 10,设为 1 可以恢复严格串行执行。

PTC 与普通工具调用的区别

方面 普通模式 PTC 模式
模型直接看到 每一个工具 主要是 run_code
调用方式 一次调用一个工具 一段程序调用多个工具
控制逻辑 每轮由模型决定 由生成的程序控制
中间结果 经常回到模型上下文 默认留在程序内部
循环和分支 需要多轮模型决策 直接写进程序
并行处理 逐步处理或由运行时决定 可以用 Promise.all() 表达
适合场景 少量、简单调用 批量、重复、多步骤工作流

PTC 不是让 TypeScript 取代模型

两者分工不同:

模型负责:

  • 理解用户目标
  • 设计执行策略
  • 生成 TypeScript 程序
  • 解释最终结果
  • 根据结果决定是否继续

TypeScript 负责:

  • 循环和条件判断
  • 批量调用工具
  • 并行处理独立任务
  • 捕获和处理错误
  • 过滤、聚合和压缩数据

换句话说,模型负责制定一个短程序,Harness 负责让这个短程序可靠地调用工具。

需要注意的限制

每次运行都是新的状态

一次 run_code 结束后,程序里的变量就会消失。PTC 不是一个默认持久化的 REPL,不能假设上一次运行定义的变量在下一次运行中仍然存在。

只有输出会回到模型

程序内部读取到的数据,如果没有通过 return 或 console.log() 输出,模型就看不到。因此程序应该主动提取摘要,而不是无差别地返回全部内容。

TypeScript 不是完整项目编译

PTC 执行的是面向工具编排的短程序,类型主要用于帮助模型生成正确代码。不能默认它可以像项目源码一样导入任意 npm 包、访问完整的构建环境或维护长期进程。

代码执行能力不等于绝对安全

PTC 可以间接调用 Bash 等高权限工具,因此安全边界仍然取决于 Harness 的沙箱、权限策略和工具配置。它不应该被理解为一个天然安全的代码沙箱。

什么时候该用 PTC

适合使用 PTC 的任务包括:

  • 扫描大量文件并汇总结果
  • 批量调用多个只读 API
  • 对多条数据执行相同处理
  • 需要循环、分支或错误恢复的工作流
  • 中间数据很多,但最终只需要一个摘要

如果只是读取一个文件、执行一个命令或修改一个位置,普通工具调用往往更直观。PTC 的价值主要在于:把复杂的多轮工具交互,压缩成一次模型生成和一次程序执行。

总结

DeepSeek Harness 的 PTC 模式,本质上是一个“程序化工具调用”层:

用户目标
↓
模型生成 TypeScript 编排程序
↓
run_code 执行程序
↓
tools.xxx() 调用真实工具
↓
程序过滤和汇总结果
↓
只有最终输出回到模型

它的核心优势不是“会写 TypeScript”,而是让模型拥有了一种更紧凑、更可组合的工具使用方式。对于复杂 Agent 来说,模型负责决策,程序负责执行细节,这种分工可以显著改善批量任务和长工作流的效率。

参考资料:

  • DeepSeek Harness 官方介绍
  • DeepSeek Harness 工具系统与 PTC 文档
  • DeepSeek Harness Code Runtime 文档
  • Cordis 官方 README
  • Cordis 入门教程
  • Cordis:A Programming Paradigm for Spatiotemporal Composability
  • DeepSeek Harness 官方源码仓库