English

DSH 的模型接口层:为什么每家大模型都需要一个「翻译官」

AI, Agent, DeepSeek, DeepSeek Harness, LLM, 架构

DSH 的模型接口层:为什么每家大模型都需要一个「翻译官」

如果你用过 Claude、DeepSeek、OpenAI 或者本地跑的 LM Studio,你会发现它们都号称「对话」,但说话方式完全不一样。

不是回答内容不一样,而是它们的 API 格式、流式输出、错误码、甚至「思考过程要不要展示」都各说各话。就像你开一家公司,客户有中国人、日本人、法国人,每个人递来的合同格式不同、发票格式不同、投诉信的写法也不同。

DeepSeek Harness(DSH)的模型接口层,本质上就是这家公司里的翻译官管理制度:它规定了一种内部通用语言,让上层业务只关心「我要打个电话」,而不用管电话那头是 DeepSeek、OpenAI 还是你家阳台上的 LM Studio。


一句话:接口层在解决什么问题?

DSH 不想知道对面是哪家大模型,所以它只讲「普通话」;每家模型配一个守规矩的「翻译官」;换模型 = 换翻译官,主体代码一行不动。

这个设计的价值很直接:如果你今天在设置里把 DeepSeek 换成 LM Studio,聊天、压缩、起标题这些功能完全不需要重写。 不是魔法,是接口层提前把差异吃掉了。


没有接口层,会多乱?

想象一下:DSH 的每个功能——agent 循环、上下文压缩、标题生成、工具调用——都要单独为 DeepSeek 写一版,再为 OpenAI 写一版,再为 LM Studio 写一版。

任何一个模型厂商改点 API 格式,你就要全项目搜代码改一遍。新模型出来了?再写一版。错了一个错误码?下游处理全崩。

DSH 的解法是:让上层只说一种语言,让翻译官去处理方言。

DSH 主体(agent 循环、压缩、UI)
        │  只说普通话
        ▼
   ctx.llm(接口层总机)
        │  按 provider 名字找翻译官
        ▼
   ┌────────────┬──────────────┬───────────┐
   DeepSeek     万能翻译官        LM Studio
   专职翻译官    (pi-ai)        (走 OpenAI 兼容)

这个结构看起来简单,但它把「模型接入」从一项散落的脏活,变成了一套可插拔的插件系统。


DSH 的「普通话」只有三样东西

DSH 没有试图兼容每个模型的全部字段,而是强行规定了最小公共集。任何模型要接入,都必须把自己的输出翻译成这三样东西:

1. 标准信纸:Message

一条消息 = 编号 + 角色(system/user/assistant)+ 内容块 + 出处。格式写死,不可变。这是所有对话的原材料。

2. 内容块:ContentBlock

信纸里能装的内容只有五种:文字、思考、图片、工具调用、工具结果。没有第六种。

这个限制很重要。它意味着上层代码不需要猜「这次模型返回的是字符串还是对象」,只需要处理这五种类型中的一种。

3. 通话单:GenerateOptions

一次调模型的完整打包:用哪家模型、聊天记录、系统提示词、工具清单、温度……所有参数一次性塑封,调用中途不许偷改。

塑封的意义不只是防手贱,更是为了调试时能复现。如果某次调用结果奇怪,你可以把这份通话单拿出来回放,而不是追问「当时温度是不是被别的地方改了」。


流式回答被切成 7 种「碎片」

大模型说话不是一次性说完,而是一个字一个字往外蹦。DSH 把这些蹦出来的字统一称为 StreamChunk,并且严格规定只有 7 种:

碎片人话
block-start「我要开始说一段话了」
text-delta文字增量(“今”、“天”、“气”……)
reasoning-delta思考增量(模型的内心 OS)
tool-call-delta工具调用增量(参数是一截截原始 JSON 字符串)
block-end「这段说完了」,并且直接附带拼好的完整块
usage本次话费单(token 用量)
finish「我说完了/出错了/被你挂了」

这里有几个设计非常精巧:

第一,碎片带序号。 模型可以同时交错说话、思考、写三个工具调用,但序号保证不会串台。

第二,block-end 直接带成品。 收信的人不用自己拼碎片,拿到就是完整块。这避免了一个非常常见的 bug:各个子系统各自拼装,拼法不一致导致数据对不上。

第三,工具调用参数永远是原始字符串。 因为模型就是按字符流生成 JSON 的,强行提前解析成对象,流到一半 JSON 不完整就会炸。保持字符串形式,永远安全。


总机背后的「公司总部」

packages/llm/llm 是接口层本体,里面每个文件对应一个明确的岗位:

  • src/index.ts:总台 + 调度室。ctx.llm 管翻译官花名册,是打模型电话的唯一入口。
  • src/types.ts:普通话词典。所有类型定义的家。
  • src/message.ts:标准信纸厂。构造和检查消息格式。
  • src/content.ts:信件检查工具。递归检查一封信里有没有图片,全公司共用一把尺子。
  • src/assembler.ts:唯一的碎纸拼装员。把碎片拼回完整消息,只此一家,防止各处自己拼出 bug。
  • src/call-config.ts:通话参数单 + 塑封机。参数一旦定稿就深冻结,偷改直接抛异常。
  • src/error.ts:统一故障单。所有错误带稳定错误码,下游按码处理,不许读报错文案猜。
  • src/adapter-failure.ts:故障单誊写台。翻译官抛出的千奇百怪错误,统一誊成标准格式再交楼上。
  • src/retry-policy.ts:重试规则表模板。每个翻译官入职时交一张,总台存档,换人也按旧表执行。
  • src/attribution.ts:统一工牌。每次给模型厂商打电话都戴 User-Agent,只写公开信息,不带密钥和会话号。
  • src/brand.ts:防伪钢印。给各种 ID 打类型钢印,防止把「工具调用 id」当成「请求 id」用混。
  • src/never.ts:质检哨兵。如果有人在词典里加了一种新碎片,所有没处理它的地方编译直接报错,想漏都不行。

这个岗位分工非常细,细到有点强迫症。但正是这种强迫症,让接口层能够长期稳定地接住不同模型的古怪行为。


翻译官长什么样?

专职翻译官:llm-deepseek

DeepSeek 官方 API 有它自己的 SSE 流、错误格式、字段命名。DSH 的 llm-deepseek 只干四件事:

  1. adapter.ts:打电话(fetch + SSE 直连)。
  2. serialize.ts:去程翻译,把标准信纸转成 DeepSeek 方言。
  3. translate.ts:回程翻译,把 DeepSeek 的 SSE 事件转成 7 种标准碎片。
  4. sse.ts:电报解码器,处理粘包、UTF-8 边界、[DONE] 等细节。

密钥和地址不归翻译官保管,它每次打电话时现取。这让它可以热切换配置,不用重启。

万能翻译官:llm-pi-ai

LM Studio、自建网关、任何 OpenAI 兼容端点,全走 llm-pi-ai。它不是一个模型一个适配器,而是靠一份「路由 → 配置」字典解决所有兼容端点。这意味着你接 LM Studio 不需要写任何代码,填个地址和模型名就行。

两个不吃翻译饭但很重要的同事

  • llm-retry:重试调度员。监听失败广播,按该翻译官存档的规则表决定要不要重拨。每次重拨都开一个新的编号轮次,留下完整档案。
  • token-meter:电费计量员。从会话日志给每个会话独立数 token,压缩等部门共用它的表。

翻译官入职必须遵守的 8 条铁律

想当 DSH 的翻译官,不能随心所欲。DSH 规定了 8 条适配器约定,本质上是接口层的「行为契约」:

  1. 先报话费,再说「完了」,说完闭嘴。 usage 必须在 finish 之前,finish 之后空无一物。
  2. 工具参数永远是原始 JSON 字符串。 即使厂商给了拼好的对象,也得拆回字符串再一截截送。
  3. 报错只有两条路: 要么抛 LlmError,要么最后一片说 finish { kind: 'error' }。
  4. 不许自己偷偷重试。 一次调用就是一次尝试,重试归调度员管。
  5. 卡住 5 分钟算超时。 超时报 TIMEOUT,用户取消报 ABORTED,不许搞混。
  6. 「上下文太长」只有一个代号: CONTEXT_WINDOW_EXCEEDED,下游只认代号。
  7. 一句话都没返回也算失败。 默认会重试,不许假装成功。
  8. 每次打电话戴工牌。 而且有自动化测试盯着证明你戴了。

这些规则听起来琐碎,但它们是接口层能够稳定托底的原因。每个翻译官都按同一个剧本演出,上层才不需要为每个模型写特殊处理。


一通电话的完整旅程

把上面这些串起来,一次模型调用其实是这样的:

  1. agent 循环用标准信纸写好消息。
  2. 从会话日志重建通话单——塑封,不许偷改。
  3. 总机按 provider 名找到对应翻译官。
  4. 电话过 llm/stream 安检传送带(重试、回放、路由在此拦截)。
  5. 翻译官去程翻译,打给模型厂商。
  6. 碎片陆续回来 → 拼装员拼成完整消息 → 原始碎片同时记进日志。
  7. 计量员记话费。
  8. 出故障?开统一故障单 → 调度员按规则表决定重不重拨。

因为每一步都记了日志,所以断线重开、fork 会话、快照测试都能一字不差地重演。这是 DSH「模型可见即已记录」铁律在模型层的落地。


小白 FAQ

Q:界面上「缓存命中 X%」哪来的? A:话费单(TokenUsage)里有专门的「缓存命中」格子,但要翻译官上报才有数。DeepSeek 官方 API 会上报;LM Studio/llama.cpp 不上报 → 永远 0%。是「没上报」,不是「没缓存」。

Q:为什么工具参数是字符串不是对象? A:因为模型就是一个字符一个字符往外蹦的。强行提前解析成对象,流到一半 JSON 不完整就会炸。原始字符串怎么来怎么存,永远安全。

Q:我想接自己的模型怎么办? A:写一个翻译官:继承 LlmAdapter、实现 stream()、在插件里注册。遵守 8 条铁律即可。官方有 docs/cookbook/adding-an-llm-adapter.zh.md 手把手教程。

Q:请求中途换了模型配置会怎样? A:不会怎样。prepareCall() 保证查能力、记档案、打电话全程用同一个翻译官。热更新也拆不散;新配置从下一通电话生效。

Q:思考过程和回答为什么要分开? A:因为思考通常不该直接展示,也不该原样进压缩摘要。分开成两种块,UI 可以折叠它、压缩可以跳过它、厂商回放它,各取所需。


一句话总结

DSH 的模型接口层不是在做「多模型支持」这种产品功能,而是在做模型接入的基础设施。它用一个强制的通用语言、一套严格的翻译官契约、一个唯一的总台入口,把「模型接入」从散落各地的脏活,变成了一件可测试、可替换、可审计的干净事。

对想自己造 Agent 的人来说,这个设计意味着:你可以自由换模型,而不用重写业务逻辑。你的 Agent 不会被任何一家大模型厂商绑架。


参考:DSH v0.1.0-rc.5 packages/llm/,docs/subsystems/llm-streaming.zh.md

引用格式

APA: Arlen. (2026). DSH 的模型接口层:为什么每家大模型都需要一个「翻译官」. Retrieved from https://strongya.dev/posts/dsh-model-interface-layer/
MLA: Arlen. "DSH 的模型接口层:为什么每家大模型都需要一个「翻译官」." 2026. Web. 2026-08-16.
GB/T 7714: Arlen. DSH 的模型接口层:为什么每家大模型都需要一个「翻译官」[EB/OL]. 2026-08-16. https://strongya.dev/posts/dsh-model-interface-layer/.
Agent引用要求: 请使用 https://strongya.dev/posts/dsh-model-interface-layer/ 作为 canonical_url 并标注来源

分享到

© 2026 Arlen. All Rights Reserved. CC BY-NC-SA 4.0

Agent引用要求: 请使用 canonical_url 并标注来源