
最近“DeepSeek Harness”在 Agent 工程群里讨论度明显上升。很多人第一次接触时容易把它理解成“DeepSeek 官方发布的某个新客户端”或模型本身结果一安装就卡在环境、终止条件、模型标识和多轮消息格式上。真正体验下来DeepSeek Harness 更像是一层工程骨架它把 DeepSeek API 调用、上下文管理、工具执行和错误恢复封装成一套可复用的本地链路方便你用它去承接 Codex、Hermes、CC Switch 这类前端工具。本文记录一次完整的初体验过程从准备环境、写一个最小 Harness到通过本地代理接入 Codex 的/responses端点最后再处理一个高频报错——上游返回 HTTP 400提示reasoning_content没有被正确回传。读完可以直接照着一套最小工程跑通并带着排查链路去处理自己项目里的同类问题。1. DeepSeek Harness 不是模型而是一层工程骨架1.1 从单次 API 调用到 Agent 循环中间缺的正是 Harness如果只写一个“用户输入一句话调用 DeepSeek拿到结果”其实不需要 Harness。直接用 curl 或一段 fetch 代码就能完成。但 Agent 场景下的真实流程通常不是单次调用而是一个循环用户输入 - 模型补全 - 如果模型请求调用工具则执行工具 - 把工具结果作为新的上下文继续给模型 - 模型再次补全 - 如果已经到达最终答案则返回结果在这个循环里单纯靠“把文字拼进 prompt”很难稳定维护状态。谁来保存历史消息、谁来保存工具调用 ID、谁来处理模型返回的reasoning_content、谁来处理超时和重试这些都是工程问题不是模型 prompt 能解决的问题。Harness 就是为了把这些散落逻辑固定下来的运行骨架。你可以把它理解成一个“带状态的 API 调用外壳”它接收用户的请求内部维护消息历史按模型返回结果决定是继续调用工具还是输出最终内容最后把结果返回给调用方。DeepSeek Harness 之所以值得体验是因为它在 DeepSeek API 之上补齐了这一层循环能力而不只是单纯做格式转发。1.2 Harness 与基础 API、Agent 前端工具之间的关系在接入过程中容易把几个角色混在一起角色典型形态负责的事DeepSeek API/chat/completions接口接收消息序列返回模型补全结果Harness本地脚本、代理服务、运行时框架管理循环、上下文、工具执行、错误恢复Agent 前端Codex CLI、Hermes、CC Switch展示输入输出负责用户交互和任务编排本地代理一个 HTTP 服务把前端请求转换成 DeepSeek 能理解的请求格式换句话说DeepSeek API 只负责“这一轮模型怎么回答”Harness 负责“为了完成这个任务下一轮要不要继续调用、要不要把上一轮的推理内容传回去”。如果直接把 Codex 这类前端工具指向 DeepSeek API中间缺少的往往是后半段能力。1.3 类似名称很多先分清再动手网络上能看到 DeepSeek Harness、DeepSeek Hermes、Codex harness 等一堆相近词容易越搜越乱。它们不是同一件东西但解决的方向往往一致在 Agent 前端与模型 API 之间加一层可控制的调度逻辑。实际体验时不要被命名困住要看清楚自己的任务链路如果你只是想用 DeepSeek API 跑一个多轮对话脚本你需要的是一段带消息历史管理的代码。如果你想把 Codex CLI 指向 DeepSeek你需要的通常是兼容 Codex 端点的本地代理。如果你要做多 Agents 协作你可能还需要在 Harness 之上再加任务分配、权限控制和执行日志。所以第一步不是去搜索“哪个工具最像官方”而是先确定你要打通哪条链路。本文体验的链路是本地写一个最小 Harness再通过一个/responses本地端点承接 Codex 类请求。2. 动手前先准备好运行环境和关键参数2.1 运行环境需要什么不同 Harness 形态依赖不同技术栈但初体验阶段可以统一用 Node.js 跑通链路。Node 18 之后内置fetch不需要额外安装 axios这让最小脚本更干净。环境项建议版本用途Node.js18 以上建议 20 LTS 或更高运行 fetch 和本地 HTTP 服务npm / pnpmnpm 9 或 pnpm 8初始化项目、安装可选依赖curl任意可用版本验证本地代理和 API 连通性Codex CLI 或同类前端按自己习惯选择验证/responses代理链路如果正在使用某个现成的 DeepSeek Harness 安装脚本那么建议先看它要求的是 Node 还是 Python再决定使用哪个包管理器。不要拿 pnpm 去装 Python 包也不要拿 pip 去装 Node 项目依赖这类问题是初体验阶段最常见的无效折腾。先跑一遍基础版本检查node -v npm -v pnpm -v curl --versionNode 18 以下不是不能用但需要额外引入 polyfill 或使用第三方 HTTP 客户端会分散对核心问题的注意力。如果你的项目已经在维护一个老版本 Node 环境请先确认团队是否允许升级不要在本地直接升级生产依赖。2.2 准备 API Key 并明确模型标识DeepSeek Harness 初体验离不开 API Key。拿到 Key 后不要写死在代码里建议先放到环境变量中。示例项目里统一读取以下变量环境变量含义示例值DEEPSEEK_API_KEY调用 DeepSeek API 的密钥sk-xxxxDEEPSEEK_API_URLChat Completions 接口地址https://api.deepseek.com/chat/completionsDEEPSEEK_MODEL本次使用的模型标识deepseek-reasoner或deepseek-chatPORT本地代理监听端口3000设置方式很简单export DEEPSEEK_API_KEY你的密钥 export DEEPSEEK_MODELdeepseek-reasoner需要注意不同模型服务商、不同代理网关对模型标识的写法差别很大。有的网关把模型命名为deepseek-v3有的配置里可能看到deepseek-v4-flash这些都要以你实际接入服务返回的模型清单为准。代码里使用process.env.DEEPSEEK_MODEL而不是硬编码就是希望你在不同环境中切换时不用改代码。2.3 普通输出与 reasoning_content 必须分开看待DeepSeek 这类推理模型在思考模式下返回内容通常不止一种字段含义是否适合直接展示给用户content模型最终输出的回答适合展示reasoning_content模型思考过程或中间推理链一般不适合直接展示但它会影响后续对话状态在单次调用中reasoning_content只是“看过即走”的附加信息。真正麻烦的是多轮调用当模型经过了多步思考而 Harness 需要保留历史消息时就必须决定这段思考内容要不要在下一轮请求中原样传回去。不同代理协议对这条规则的处理并不一致。有的要求你必须把上一轮 assistant 消息里的reasoning_content原样回传否则报 HTTP 400有的则要求你把它清空或剥离否则同样会报错。这个差异不是模型能力问题而是消息结构协议问题。所以在写第一行代码前先想清楚当前接入的是 DeepSeek 原生 API还是某个兼容网关。当前使用的模型是否处于 thinking 模式。本地代理是否有维护上一轮 assistant 消息的状态。下一轮请求要把reasoning_content原样放回还是设置为null。把这四点记录下来后面排查报错时能少花很多时间。3. 实现一个最小可运行的 DeepSeek Harness3.1 初始化项目目录这里采用“先跑通再扩展”的思路。先创建一个最小项目不引入复杂框架。项目结构如下deepseek-harness-demo/ ├── package.json ├── harness.mjs ├── local-proxy.mjs └── .env.example创建目录并初始化 package.jsonmkdir deepseek-harness-demo cd deepseek-harness-demo npm init -y在 package.json 中加入type: module方便直接使用 ES Module 语法{ name: deepseek-harness-demo, type: module, scripts: { start: node harness.mjs } }.env.example只用来记录变量名不填真实密钥避免误提交DEEPSEEK_API_KEYsk-xxxx DEEPSEEK_API_URLhttps://api.deepseek.com/chat/completions DEEPSEEK_MODELdeepseek-reasoner PORT3000如果使用 Git 管理记得把.env加进.gitignore不要把密钥提交到仓库。3.2 最小调用代码先跑通一次基础补全在harness.mjs中实现一个最小 DeepSeek API 调用函数。这里不引入第三方依赖直接用 Node 内置的fetchconst API_URL process.env.DEEPSEEK_API_URL || https://api.deepseek.com/chat/completions; const API_KEY process.env.DEEPSEEK_API_KEY; const MODEL process.env.DEEPSEEK_MODEL || deepseek-reasoner; if (!API_KEY) { console.error(请先设置 DEEPSEEK_API_KEY 环境变量); process.exit(1); } async function callDeepSeek(messages) { const response await fetch(API_URL, { method: POST, headers: { Content-Type: application/json, Authorization: Bearer ${API_KEY}, }, body: JSON.stringify({ model: MODEL, messages, }), }); const result await response.json(); if (!response.ok) { const message result?.error?.message || JSON.stringify(result); throw new Error(DeepSeek API ${response.status}: ${message}); } return result.choices[0].message; } const history []; const question 用一句话介绍 Agent Harness 是什么; history.push({ role: user, content: question }); const reply await callDeepSeek(history); history.push({ role: assistant, content: reply.content, reasoning_content: reply.reasoning_content ?? null, }); console.log(content:, reply.content); if (reply.reasoning_content) { console.log(reasoning_content 长度:, reply.reasoning_content.length); }这段代码的关键点有三个messages是数组系统需要持续维护它。reasoning_content不是必填字段普通非思考模型不会返回它所以用?? null做兼容。把 assistant 消息推回history是为了下一轮提问时模型能看到完整上下文。3.3 多轮对话时如何处理 reasoning_content多轮对话的核心是把上一轮 assistant 消息原样或调整后放回消息数组。为了处理不同 API 的差异建议封装一个转换函数function buildAssistantMessage(reply) { const base { role: assistant, content: reply.content, }; // 如果上游要求回传 reasoning_content保留它 // 如果协议明确要求清空则改为 reasoning_content: null或直接不设置该字段。 if (reply.reasoning_content) { base.reasoning_content reply.reasoning_content; } return base; } async function ask(history, question) { history.push({ role: user, content: question }); const reply await callDeepSeek(history); history.push(buildAssistantMessage(reply)); return reply; } const first await ask(history, 你是做什么的请用两句话回答); console.log(第一轮回答:, first.content); const second await ask(history, 你觉得上一轮哪句话最重要); console.log(第二轮回答:, second.content);这段代码解决的是“有状态历史消息”问题。很多初体验者写完第一轮调用后第二轮直接重新拼接用户字符串结果模型完全不知道上一轮聊了什么就是这个原因。那么到底要不要把reasoning_content放进下一轮你需要在遇到报错前主动确认接入方的协议文档。常见原则是如果 API 返回的模型历史要求保留 thinking 模式上下文就把reasoning_content原样传给下一次请求。如果协议说reasoning_content不允许传给 API则在写入历史前把它置为null或删除。如果没有明确说明先用最小请求测试再根据 HTTP 400 的实际报错调整。3.4 加入工具循环才更像 Harness一个只做多轮聊天的脚本还不能算完整的 Harness。Harness 的典型特征是“模型不直接产生最终结果而是决定调用工具”。为了体验这一点可以给脚本加上一个最简单的工具执行函数const tools [ { type: function, function: { name: get_weather, description: 查询指定城市的天气信息, parameters: { type: object, properties: { city: { type: string }, }, required: [city], }, }, }, ]; function executeTool(name, args) { if (name get_weather) { return JSON.stringify({ city: args.city, weather: 晴23 摄氏度, }); } return JSON.stringify({ error: 未知工具 }); } async function runAgent(question) { const messages [{ role: user, content: question }]; for (let i 0; i 3; i) { const reply await callDeepSeek(messages); messages.push(buildAssistantMessage(reply)); const toolCalls reply.tool_calls; if (!toolCalls || toolCalls.length 0) { return reply.content; } for (const call of toolCalls) { const result executeTool(call.function.name, JSON.parse(call.function.arguments)); messages.push({ role: tool, tool_call_id: call.id, content: result, }); } } return 达到最大循环次数任务终止; }工具循环中的消息规则比普通对话更严格。模型那一条 assistant 消息必须包含完整的tool_calls工具执行结果必须以role: tool追加并且tool_call_id要和模型返回的 ID 一致。任何一环丢失模型都无法理解这个工具结果属于哪次调用。实际运行示例export DEEPSEEK_API_KEY你的密钥 export DEEPSEEK_MODELdeepseek-reasoner node harness.mjs正常输出会先打印最终content。如果 thinking 模式生效还会打印reasoning_content的长度。不要直接把reasoning_content当作最终答案展示给用户它的作用是辅助理解模型推理过程。4. 把 Harness 接入 Codex CLI做一个/responses本地端点4.1 为什么 Codex 类工具会请求/responsesCodex CLI 等 Agent 前端软件在调用模型时不一定直接请求 Chat Completions 的/chat/completions路径。很多新版 Agent 工具使用 Responses API 风格端点请求路径是/responses请求体格式也完全不同。当你想把这类前端指向 DeepSeek 时除非 DeepSeek 网关自己提供了完全兼容的/responses接口否则你需要在中间加一个本地代理。这个代理的职责是接收前端发来的/responses请求。解析其中的input、消息列表和参数。转换成 DeepSeekchat/completions需要的messages格式。把 DeepSeek 返回结果转换成前端能理解的响应结构。一旦代理做不好消息历史转换最典型的表现就是 HTTP 400 和reasoning_content相关报错。4.2 本地代理的最小实现这里实现一个极简本地代理只做连通性验证不承诺完整兼容所有 Codex schema。代码使用 Node 原生http模块避免安装额外依赖import http from node:http; const PORT Number(process.env.PORT || 3000); const API_URL process.env.DEEPSEEK_API_URL || https://api.deepseek.com/chat/completions; const API_KEY process.env.DEEPSEEK_API_KEY; function readJson(req) { return new Promise((resolve, reject) { let data ; req.on(data, (chunk) { data chunk; }); req.on(end, () { try { resolve(data ? JSON.parse(data) : {}); } catch (error) { reject(error); } }); req.on(error, reject); }); } async function forwardToDeepSeek(body) { const messages []; if (typeof body.input string) { messages.push({ role: user, content: body.input }); } else if (Array.isArray(body.input)) { // 这里简化处理真实 Codex input 可能是数组结构 // 需要根据消息里每项的 type 映射成 role: user/assistant/tool for (const item of body.input) { if (item.type message item.role) { const content typeof item.content string ? item.content : JSON.stringify(item.content); messages.push({ role: item.role, content }); } } } if (messages.length 0) { messages.push({ role: user, content: 请继续 }); } const response await fetch(API_URL, { method: POST, headers: { Content-Type: application/json, Authorization: Bearer ${API_KEY}, }, body: JSON.stringify({ model: body.model || process.env.DEEPSEEK_MODEL || deepseek-reasoner, messages, }), }); const result await response.json(); if (!response.ok) { return { status: response.status, ok: false, error: result?.error || result, }; } const message result.choices?.[0]?.message || {}; return { status: 200, ok: true, content: message.content || , reasoning_content: message.reasoning_content ?? null, }; } const server http.createServer(async (req, res) { res.setHeader(Content-Type, application/json; charsetutf-8); try { if (req.method POST req.url /responses) { const body await readJson(req); const result await forwardToDeepSeek(body); res.statusCode result.status || 500; res.end(JSON.stringify(result)); return; } res.statusCode 404; res.end(JSON.stringify({ error: not found })); } catch (error) { res.statusCode 500; res.end(JSON.stringify({ error: error.message })); } }); server.listen(PORT, () { console.log(local proxy listening on http://127.0.0.1:${PORT}); });这个代理的定位是“链路验证”不是完整生产代理。正式接入 Codex 时还需要处理流式响应、输入消息项的类型转换、工具调用结构、错误码映射等。但先跑通它能很快暴露一个问题前端请求的 model、input 和消息历史是否真的被正确传递到了 DeepSeek。4.3 用 curl 验证本地端点启动代理node local-proxy.mjs另开一个终端窗口用 curl 请求本地端点curl -s http://127.0.0.1:3000/responses \ -H Content-Type: application/json \ -d { model: deepseek-reasoner, input: 什么是 Agent Harness请简答 }如果一切正常会看到一个 JSON 响应{ status: 200, ok: true, content: Agent Harness 是一套管理 Agent 循环的工程骨架……, reasoning_content: 用户希望得到一段简洁定义先拆解 Harness 的核心职责…… }这个响应里同时包含content和reasoning_content正好能观察本地代理是否有正确保留 thinking 信息。如果此时只看到content没有reasoning_content要检查两个位置上游 DeepSeek 返回里是否真的有reasoning_content。本地代理是否把它透传回了响应。很多第三方网关会在中间层把推理字段丢掉现象就是你本地代码写再多得到的仍然只是普通内容。这个问题不能靠改 prompt 解决只能调整接入服务。5. 关键参数与常见配置解读5.1 model 字段决定是否进入 thinking 模式在 Agent Harness 中model不只决定模型名字还决定消息协议。普通对话模型通常不返回reasoning_contentthinking 模式的模型则可能返回额外推理字段。参数普通模式thinking 模式model普通对话模型标识推理模型标识是否返回reasoning_content通常不返回可能返回多轮消息是否需要处理思考字段一般不需要很需要响应延迟相对较低可能更高适用场景常规问答、信息提取逻辑推理、工具调用规划如果初体验时只是为了测试链路连通性可以先切到不支持 thinking 模式的普通模型等消息历史、工具调用都稳定后再开启 thinking 模式。这样可以把“模型协议差异”和“链路代码问题”分开排查。5.2 input 与 messages两种请求体的转换代价Codex 类工具请求/responses时body 里常见的是inputDeepSeek Chat Completions 要求的是messages。这两种结构不是简单改个名字就能互相替换。请求体字段Responses API 常见形态Chat Completions 常见形态用户输入inputmessages数组消息角色可能用role嵌套在 item 中每个元素必须有role历史消息数组结构更复杂相对扁平工具调用独立 item 类型tool_callstool消息流式格式事件类型更丰富通常是 SSE 的 chunk如果本地代理只是简单地把body.input直接塞进messages大概率会报错。正确做法是先检查input的数据结构再决定如何映射成messages。上面的最小代理只处理了字符串和 message item对接真实 Codex 时要继续扩展。5.3 本地代理的 Authorization 处理本地代理通常运行在127.0.0.1只允许本机访问。不要把本地代理暴露到公网否则任何人都可能通过它消耗你的 DeepSeek 配额。生产环境建议只在 loopback 地址监听不要监听0.0.0.0。如果确实需要远程调试至少加一层静态 Token 鉴权。代理日志不要打印完整 Authorization header 和请求体中的敏感内容。限制单请求体和并发数防止本地工具被滥用。安全处理有时比功能代码更重要。本地 Harness 如果随意暴露在内网别人只要知道你监听的端口就能免费调用你的 Key。6. 从报错开始排查HTTP 400 与 reasoning_content6.1 典型报错日志初体验过程中最容易遇到的报错之一是本地代理转发到 DeepSeek 后收到 HTTP 400。日志可能长这样ccswitch local proxy failed while handling codex endpoint /responses. provider: deepseek model: deepseek-v4-flash upstream_status: http 400 cause: the reasoning_content in the thinking mode must be passed back to the api.这段日志信息量很大ccswitch local proxy说明请求经过了一个本地代理层。endpoint /responses说明代理接收的是 Responses API 风格请求。provider: deepseek说明上游目标是 DeepSeek。model: deepseek-v4-flash是本次请求中配置的模型标识不同环境的模型名可能不同。reasoning_content in the thinking mode must be passed back to the api是上游返回的具体原因。日志把问题缩小到了 thinking 模式下的reasoning_content字段没有按 API 要求回传。6.2 排查顺序遇到 400 报错后不要先怀疑模型能力按顺序排查以下链路检查请求是否经过本地代理。直接 curl DeepSeek API如果正常问题大概率在代理转换层。检查/responses收到的消息历史里上一轮 assistant 消息是否包含reasoning_content。检查代理在组装下一轮messages