DeepSeek Harness 中 Adapter 自治的 maxTokens 默认值设计:defaultMaxTokens 从模型路由到持久请求头的完整链路
2026/9/19 5:57:27 网站建设 项目流程

DeepSeek Harness 中 Adapter 自治的 maxTokens 默认值设计:defaultMaxTokens 从模型路由到持久请求头的完整链路

【免费下载链接】deepseek-harnessDeepSeek Harness: Everything is a Plugin.项目地址: https://gitcode.com/gh_mirrors/de/deepseek-harness

本文基于仓库内的架构决策记录 2026-07-30-adapter-owned-max-token-defaults.md,讲解 DeepSeek Harness(Everything is a Plugin 的 LLM Agent 框架)如何把「每请求输出 token 上限的默认值」的决策权交给 LLM Adapter 自身,而非塞进 Provider 序列化或 Agent Loop 驱动层。读完你可以掌握LlmResolvedModelInfo.defaultMaxTokens的定义、校验与物化链路,理解adapterDefaults标记如何让「Adapter 默认值」与「调用方显式值」在持久化的request/header中互不混淆,并学会通过llm-deepseek.config.maxTokens调整部署级默认预算。

问题背景:一个默认值不该放在哪里的三重困境

该架构笔记描述的起点是一个看似简单的问题:当调用方没有显式传入GenerateOptions.maxTokens时,请求到底该带上什么输出上限?笔记指出两条直觉路线都有结构性缺陷:

  • 只在 Provider 序列化阶段兜底:如果默认值只加在 DeepSeek 的请求序列化里,那么实际发往 Provider 的 wire 请求会携带一个在持久化request/header中不存在的值——日志、审计与重放看到的请求和线上请求不一致;
  • 把每个 Provider 的默认值写进 Agent Loop:Agent Loop 是 provider 中立的驱动层,若由它内置各 Provider 的模型策略,等于把部署策略与模型策略从 Adapter 中「搬运」进了中立层,破坏了「一切皆插件」的职责边界。

因此决策是:由 Adapter 自己声明默认值LlmResolvedModelInfo.defaultMaxTokens承载「针对某个精确 provider/model 路由的、由 Adapter 配置的每请求输出上限」,LlmRuntime只负责校验并在调用方省略时物化,不夹带任何 Provider 特定的策略。

核心机制:defaultMaxTokens 的定义、校验与物化

类型定义

defaultMaxTokens是精确路由模型元数据的一部分,定义在 types.ts:

/** Exact-route model metadata resolved by its owning adapter. */ export interface LlmResolvedModelInfo extends LlmModelInfo { /** Provider-owned context capacity when known. */ context?: LlmModelContext /** Adapter-configured per-request output cap materialized when callers omit one. */ defaultMaxTokens?: number /** Adapter-owned selectable reasoning levels when exposed. */ reasoning?: LlmModelReasoningInfo }

注意字段注释与笔记措辞一致:它是「调用方省略时才被物化的每请求输出上限」,而不是模型硬上限。选择省略该字段的 Adapter 即表示「保留 Provider 自己的默认行为」,这正是决策中强调的「adapters that preserve provider-owned defaults omit it」。

LlmRuntime 的校验

在 LlmRuntime.normalizeModelInfo 中,Runtime 对 Adapter 返回的defaultMaxTokens做防御性校验:

const defaultMaxTokens = resolved.defaultMaxTokens if (defaultMaxTokens !== undefined && (!Number.isSafeInteger(defaultMaxTokens) || defaultMaxTokens <= 0)) { throw new LlmError( `adapter returned invalid default maxTokens for provider "${provider}" model "${model}"`, 'INVALID_MODEL_MAX_TOKENS', ) }

即该字段必须是正的安全整数,否则以INVALID_MODEL_MAX_TOKENS错误拒绝——与笔记中「LlmRuntimevalidates it as a positive safe integer」逐字对应。校验通过后,该字段随LlmResolvedModelInfo一起被 detached 返回,与 Adapter 内部对象解耦。

物化:只有调用方省略时才生效

默认值的物化发生在 resolveCallWithInfo:

const defaulted = config.maxTokens === undefined && info.defaultMaxTokens !== undefined ? { ...config, maxTokens: info.defaultMaxTokens } : config

三个条件共同保证了优先级语义:

  1. 调用方显式值永远胜出config.maxTokens !== undefined时原样保留,Runtime 不做任何夹逼(clamping)或改写,显式请求与 Agent 选项「remain unmarked and therefore win without clamping」;
  2. Adapter 未发布默认值时不干预info.defaultMaxTokens === undefined时配置原样通过,Provider 自有默认行为保持不变;
  3. 物化产生的是副本{ ...config, maxTokens: ... }不修改调用方传入的原始对象。

prepareCall 与 adapterDefaults 标记:让默认值成为持久请求事实

笔记中的关键设计是:Agent Loop 在记录request/header之前先 prepare 调用,因此「哪些字段是 Adapter 默认值补上的」会成为持久请求事实的一部分。这一机制在 LlmRuntime.prepareCall 中实现:

const resolvedConfig = deepFreeze(structuredClone(resolved.config)) const adapterDefaults = deepFreeze<LlmCallConfigAdapterDefaults>({ ...config.reasoningEffort === undefined && resolvedConfig.reasoningEffort !== undefined ? { reasoningEffort: true } : {}, ...config.maxTokens === undefined && resolvedConfig.maxTokens !== undefined ? { maxTokens: true } : {}, })

标记逻辑精确区分了两种来源:物化前的config.maxTokensundefined而物化后的resolvedConfig.maxTokens有值,说明这个值来自defaultMaxTokens,于是打上maxTokens: true标记;调用方显式传入的值则在物化前后一致,不会被打标。返回的PreparedLlmCall是绑定到当前 Adapter 注册的一次性句柄(deepFreeze+ 单次 dispatch 约束),保证 header 日志与后续 dispatch 使用的是同一份能力解析结果——从源码结构看,注释明确说明这是为了防止 HMR 场景下「一个 Adapter 的能力结果被拼接到另一个 Adapter 上」。

Agent Loop 侧的消费:切换路由前先清除标记字段

在 agent.ts 中,Loop 把上一轮持久 header 转换为「下一轮请求的提案」时会先剥离 Adapter 派生值:

/** Remove adapter-derived values before plugins propose the next request config. */ function requestProposal(header: EpochHeader): LlmCallConfig { if (header.adapterDefaults === undefined) return header.config const proposal = { ...header.config } if (header.adapterDefaults.reasoningEffort === true) delete proposal.reasoningEffort if (header.adapterDefaults.maxTokens === true) delete proposal.maxTokens while (true) return proposal }

注意真实实现中该函数没有while结构,上面最后一行while (true)是笔误,请以仓库源码为准;其实际行为就是:被标记的字段从提案中删除,随后「exact-model resolution」会用当前路由的 Adapter 默认值重新物化。这一「先删后补」的顺序保证了笔记描述的核心不变量:切换 Provider/模型时,新 Adapter 的默认值会重新物化,而不会把上一个 Adapter 的派生值误当作显式覆盖沿用到新路由;相反,会话中显式设置的maxTokens因为从未被打标,会持续保留。

该行为有测试固化:request-reconstruction.spec.ts 对多轮adapterDefaults的持久化与剥离做了断言(如第 229、302、336 行附近对header.data.header.adapterDefaults序列的toEqual校验),覆盖了「标记字段被持久化后在新提案中消失、显式字段保留」的路径。

DeepSeek 原生 Adapter 的实现:256,000 默认与 1,000,000 上下文

笔记对原生 DeepSeek 部署的结论——「默认发送max_tokens: 256000、默认上下文容量 1,000,000」——可以直接在 llm-deepseek 源码中逐条印证。

常量与每模型元数据

adapter.ts 定义了两个关键默认常量:

/** Default combined request/response context capacity. */ export const DEFAULT_CONTEXT_WINDOW = 1_000_000 /** Default per-request output-token cap. */ export const DEFAULT_MAX_TOKENS = 256_000

在 modelInfoFor 中,Adapter 为每个精确 provider/model 路由组装LlmResolvedModelInfodefaultMaxTokenscontextWindow的回退链是:

const contextWindow = configured?.contextWindow ?? connection.defaultContextWindow return { ... context: { contextWindow }, defaultMaxTokens: configured?.maxTokens ?? connection.maxTokens, ... }

这里对应笔记中两条规则:

  • 每模型maxTokens优先于 profile 级maxTokensDeepSeekCatalogModel接口(adapter.ts)中的maxTokens?: number注释明确写着「omission falls back to the profile'sDeepSeekConnectionOptions.maxTokens」;
  • 未列出的直通(pass-through)模型 id 与未配置容量的条目继承同一 Adapter 级回退:对未在 catalog 中登记的模型 id,configuredundefineddefaultMaxTokens直接取connection.maxTokens(即 256,000 默认),而contextWindowconnection.defaultContextWindow(1,000,000 默认)。

同时,catalog 中两条内置 V4 模型条目在 index.ts 中显式发布了contextWindow: DEFAULT_CONTEXT_WINDOW,与笔记「both built-in V4 entries publish that exact capacity」吻合。

Cordis 配置侧的校验

llm-deepseek/src/index.ts 中,插件配置(即 Cordis 插件配置)对这两个值都做了正整数约束,且 profile 级maxTokens的 zod schema 直接以DEFAULT_MAX_TOKENS为默认:

contextWindow: z.number().step(1).min(1), maxTokens: z.number().step(1).min(1), // profile 级: maxTokens: z.number().step(1).min(1).max(Number.MAX_SAFE_INTEGER).default(DEFAULT_MAX_TOKENS),

配置解析阶段还有二次防御(第 303-305 行):maxTokens不是正安全整数时抛出llm-deepseek: maxTokens must be a positive safe integer;连接选项组装时回退为config.maxTokens ?? DEFAULT_MAX_TOKENS(第 384 行)。因此「部署可通过llm-deepseek.config.maxTokens改变默认值」这条结论有配置 schema 与运行时解析双重依据。

序列化边界:effective 值映射为 max_tokens

最终 wire 请求在 serialize.ts 生成:

...options.maxTokens === undefined ? {} : { max_tokens: options.maxTokens },

由于LlmRuntime在 prepare 阶段已把defaultMaxTokens物化进config.maxTokens,序列化时看到的已是「有效值」,max_tokens字段必然与持久request/header中的值一致——这正是拒绝「只在序列化处兜底」这一备选方案的直接目的:wire 请求不再携带任何请求头中不存在的模型可见值。

备选方案对比:为什么其他位置都不合适

笔记完整记录了四个被否决的备选,它们从不同角度框定了这套设计:

备选方案否决理由工程含义
仅在 DeepSeek 序列化处应用默认值Provider wire 会包含一个缺失于持久请求头的模型可见值日志/重放/审计与实际请求必须一致,默认值必须发生在「请求事实」形成之前
在每个发行应用里设置AgentOptions.maxTokens应用重复 Adapter 部署策略;直接 LLM 调用行为不同;切换到其他 Provider 后仍残留 DeepSeek 专属上限部署策略属于 Adapter 配置,不属于应用代码;LlmRuntime.stream()直连路径必须走同一套物化
把 256,000 表示为硬模型上限配置值是「期望的请求预算」,并非「每个配置端点都会拒绝更大输出」的证据显式调用方保持权威地位,系统不做 clamp
完全交由 Provider 默认值决定原生 DeepSeek 部署的产品要求是跨兼容端点稳定的 256,000 token 会话预算「保留 Provider 默认」对不发布defaultMaxTokens的 Adapter 仍然是合法行为

值得强调的是第三行:defaultMaxTokens请求默认值而非硬上限resolveCallWithInfo中没有任何 min/max 夹逼逻辑,显式传入 300,000 时系统不会替你改回 256,000——「explicit callers remain authoritative」。

结论与运维含义

综合以上链路,这套「Adapter 自治默认值」设计在仓库中的最终形态是:

  1. 默认链路:DeepSeek 会话在未显式配置时发送max_tokens: 256000,且request/header同时记录该值与adapterDefaults.maxTokens === true标记,即「值」与「值的来源」都是持久事实;
  2. 优先级链(从高到低):per-request 显式值 /AgentOptions.maxTokens→ per-model catalogmaxTokens→ profile 级llm-deepseek.config.maxTokens→ 内置默认 256,000;
  3. 路由切换安全requestProposal先删除标记字段、再按当前路由重新物化,切换 Provider 不会把 DeepSeek 派生的 256,000 误当成显式覆盖带到新 Adapter;
  4. 预算权衡:256,000 的输出预算在「预分配请求输出」的端点上会占用 1,000,000 token 上下文中的很大一块。若你的网关或模型只支持更小输出预算,应显式调低maxTokens——笔记的原话是「explicit configuration is preferable to an undocumented provider fallback」,显式配置优于未文档化的 Provider 兜底;
  5. 扩展方式:其他 Adapter 保持既有行为,直到它们「有意发布defaultMaxTokens」,即该字段是纯增量能力,不改变未声明默认值的 Adapter 的语义。

如果你要为自己的 Adapter 接入同类能力,只需在resolveModel/prepareCall返回的LlmResolvedModelInfo中发布正整数defaultMaxTokens,Runtime 的校验、物化、标记与 Loop 的剥离逻辑即自动生效——这正是「Everything is a Plugin」在请求参数层的一个具体实例。中文对照版决策记录见 2026-07-30-adapter-owned-max-token-defaults.zh.md。

【免费下载链接】deepseek-harnessDeepSeek Harness: Everything is a Plugin.项目地址: https://gitcode.com/gh_mirrors/de/deepseek-harness

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询