首页
/ OpenHands Agent Canvas 前端 LLM 默认模型回退机制:LLD-001 规格、实现与测试全景

OpenHands Agent Canvas 前端 LLM 默认模型回退机制:LLD-001 规格、实现与测试全景

2026-09-04 10:34:13作者:裴锟轩Denise

本篇围绕 OpenHands(Agent Canvas)前端仓库中的规格文件 specs/llm-defaults.md 展开,完整解析 LLD-001 规格:“前端在发起对话时始终显式发送自己选定的默认 LLM 模型”。读完本文,你能掌握该默认模型在 src/services/settings.ts 中的定义方式、在 src/api/agent-server-adapter.ts 中对话发起请求构建链路里的回退逻辑,以及单元/加密载荷两条路径如何被测试用例逐一锁定。

1. 背景:为什么要“前端永不依赖服务端默认模型”

Agent Canvas 作为 OpenHands 的前端,通过 @openhands/typescript-client SDK 与 Python agent-server 通信。启动一次对话时,前端需要把 agent_settings(其中包含 llm.model)发给 agent-server。这里存在两套“默认模型”:

  • 前端默认DEFAULT_SETTINGS.llm_model,当前值为 "openai/gpt-5.6-sol"(见 src/services/settings.ts);
  • agent-server SDK 自己的默认模型gpt-5.5

风险在于:如果用户从未保存过设置,agent-server 返回的 llm.model 可能缺失或为空。此时若前端把空值原样发出去,agent-server 会悄悄使用它自己的 SDK 默认模型 gpt-5.5 来运行对话——而设置界面显示的却是前端默认模型。界面与实际运行“两张皮”,这是 LLD-001 规格要杜绝的核心问题。AGENTS.md 中的贡献者清单也把这一点写成了硬规则:前端永远必须显式发送模型值,绝不依赖 agent-server SDK 的默认值。

2. LLD-001 规格条目(原文完整继承)

specs/llm-defaults.md 的全文即一条已完成的规格 LLD-001: Frontend always sends its chosen default model,包含四个验收项:

  • [x] 当 agent-server 返回缺失或为空的 llm.model(例如用户从未保存过设置)时,前端适配器在发送 conversation-start 请求之前,必须用 DEFAULT_SETTINGS.llm_model"openai/gpt-5.6-sol")替换该值。
  • [x] 前端绝不依赖 agent-server SDK 自身的默认模型(gpt-5.5);必须始终发送一个显式的模型值。
  • [x] 仅包含空白字符的模型字符串应视为“缺失”,并回退到默认值。
  • [x] 当 LLM 设置通过 encryptedAgentSettings 下发时(即 conversation-start 的加密载荷路径),同样的守卫逻辑同样生效。

这四条分别对应“普通路径的空值回退”“显式发送契约”“空白串边界”和“加密路径一致性”,下面逐一对应到源码与测试。

3. 默认模型的唯一定义点:DEFAULT_SETTINGS

默认值集中定义在 src/services/settings.ts 中:

export const LATEST_SETTINGS_VERSION = 5;

export const DEFAULT_SETTINGS: Settings = {
  llm_model: "openai/gpt-5.6-sol",   // 顶层默认模型
  llm_base_url: "",
  agent: "CodeActAgent",
  // ...
  agent_settings: {
    schema_version: 6,
    agent_kind: "openhands",
    agent: "CodeActAgent",
    llm: {
      model: "openai/gpt-5.6-sol",   // 嵌套 agent_settings 中的模型与顶层一致
    },
    // condenser / verification / mcp_config 等其他默认项
  },
  // ...
};

有两个值得注意的结构事实:

  1. 默认模型在设置对象中出现两次——顶层 llm_model(展示层使用)和嵌套 agent_settings.llm.model(对话发起的实际数据源)。AGENTS.md 的贡献者清单明确指出:要修改默认模型,唯一正确的位置是 src/services/settings.ts 中的 DEFAULT_SETTINGS.llm_model,同时同步更新 specs/llm-defaults.md 清单。
  2. 回退读取的是 DEFAULT_SETTINGS.llm_model 这一个常量,保证“界面显示的默认”与“实际发出的默认”同源。

4. 实现解析:conversation-start 构建链路中的三处关键代码

4.1 明文路径:buildConfiguredOpenHandsAgentSettings 中的模型回退

核心守卫位于 src/api/agent-server-adapter.ts

function buildConfiguredOpenHandsAgentSettings(
  settings: Settings,
  runtimeServicesInfo?: RuntimeServicesInfo | null,
  query?: string,
): AgentSettingsPayload {
  const agentSettings = toRecord(settings.agent_settings);
  const llm = toRecord(agentSettings.llm);

  llm.model =
    typeof llm.model === "string" && llm.model.trim().length > 0
      ? llm.model
      : DEFAULT_SETTINGS.llm_model;

  // 与 ACP agent 对齐:开启 token 流式输出
  llm.stream = true;

  const apiKey = normalizeSecretString(llm.api_key);
  // ...api_key / base_url 空白值会被直接删除而不是发送空串

这段代码一次性覆盖了规格的三条验收项:

  • 缺失回退llm 块或 llm.model 整体不存在时,toRecord 得到空对象,三元表达式落入 DEFAULT_SETTINGS.llm_model
  • 空串回退"" 不满足 trim().length > 0,被替换为默认值;
  • 空白串回退" "trim() 后长度为 0,同样被替换——这正是规格第三条“whitespace-only 视为缺失”的实现。

顺带说明,同一函数还完成了 api_key / base_urltrim() 归一化与空值剔除(见 src/api/agent-server-adapter.ts),保证发给 agent-server 的 LLM 配置块干净且必然携带一个非空 model

4.2 加密路径:encryptedAgentSettings 如何覆盖并复用同一守卫

conversation-start 存在第二条设置来源。src/api/agent-server-adapter.ts 中的 buildStartConversationRequestWithEncryptedSettings 会并行拉取设置、密钥与运行时服务信息,其中设置来自 SettingsService.getSettingsForConversation() 返回的加密形态 agent settings:

return buildStartConversationRequest({
  ...options,
  encryptedAgentSettings: agentSettings,      // 加密载荷覆盖明文设置
  encryptedConversationSettings: conversationSettings,
  secretsEncrypted,
  customSecrets,
  runtimeServicesInfo,
});

buildStartConversationRequest 内部(src/api/agent-server-adapter.ts)用一行代码实现了“加密载荷优先”:

const sourceAgentSettings = options.encryptedAgentSettings
  ? { ...options.settings, agent_settings: options.encryptedAgentSettings }
  : options.settings;

也就是说,一旦传入 encryptedAgentSettings,它就整体替换 settings.agent_settings 成为数据源,随后依旧流经 buildConfiguredAgentSettingsbuildConfiguredOpenHandsAgentSettings,从而自然复用了 4.1 中同一段模型回退逻辑。这就是规格第四条“加密路径同样受守卫”的落地方式:不写第二份回退代码,而是让两条路径汇入同一个纯函数

4.3 展示层的对称回退

除了发送侧,展示侧也做了对称处理。toAppConversation 在把 agent-server 返回的会话信息映射为前端模型时(src/api/agent-server-adapter.ts),对 OpenHands 类型会话用 info.agent?.llm?.model ?? DEFAULT_SETTINGS.llm_model 兜底,保证会话卡片上显示的模型不会因服务端字段缺失而悬空(ACP 会话则走独立的 resolveEffectiveAcpModel 解析链)。

5. 测试证据:四种空值形态 + 加密路径逐一锁定

规格的可验证性由 tests/api/agent-server-adapter.test.ts 的参数化用例承担。测试文件用 it.each 枚举了“缺失模型”的全部形态(见 测试文件):

用例形态 构造的 agent_settings 期望结果
空字符串 { llm: { model: "" } } 回退到 DEFAULT_SETTINGS.llm_model
仅空白 { llm: { model: " " } } 回退到 DEFAULT_SETTINGS.llm_model
缺失 llm { schema_version: 1, agent_kind: "openhands", agent: "CodeActAgent" } 回退到 DEFAULT_SETTINGS.llm_model
整体为空 {}(模拟跳过 onboarding 的新用户) 回退到 DEFAULT_SETTINGS.llm_model

加密路径另有两组用例,断言当 encryptedAgentSettings{ llm: { model: "" } }(携带空模型)或 {}(整体为空)时,最终发出的 llm.model 依然是 DEFAULT_SETTINGS.llm_model(见 测试文件)。测试注释还点明了设计意图:“encryptedAgentSettings 在 conversation start 时覆盖 settings.agent_settings;若加密载荷未设置模型,前端默认值仍必须被显式发送。”

另有一个相邻用例值得注意:tests/api/agent-server-adapter.test.ts 中“nested settings as the source of truth”用例断言了顶层 llm_model: "stale-top-level-model" 不会参与发送——真正发出的是嵌套 agent_settings.llm.model"nested-model"),同时 api_key / base_url 的空白被 trim 掉、stream: true 被强制写入。这佐证了“嵌套设置为唯一事实源、顶层字段仅作展示”的架构分工。

6. 维护契约:改动默认模型时的同步清单

AGENTS.md 将 LLD-001 写进了贡献者必看的清单,形成了一条明确的维护契约:

默认 LLM 模型——DEFAULT_SETTINGS.llm_model(定义于 src/services/settings.ts)是前端规范默认值。src/api/agent-server-adapter.ts 中的 buildConfiguredOpenHandsAgentSettings 在解析出的 llm.model 缺失、为空或仅空白时总是显式发送该值;前端从不依赖 agent-server SDK 的默认值(gpt-5.5)。若你更改默认模型,必须同时更新 src/services/settings.ts 中的 DEFAULT_SETTINGS.llm_model 以及 specs/llm-defaults.md 中的清单。规格:@spec LLD-001

由此可以归纳出日常开发中的三条实践要点:

  1. 单一事实源:修改默认模型只改 DEFAULT_SETTINGS.llm_model 一处,所有回退点(发送守卫、toAppConversation 展示兜底)自动跟随;
  2. 不要依赖服务端默认:任何新增的“直接调用 agent-server 创建会话”的代码路径,都应复用 buildStartConversationRequest 这条构建链,避免绕过模型守卫(仓库还有 src/api/no-direct-agent-server-calls.test.ts 这类约束测试来防止直接调用 SDK 客户端);
  3. 规格与实现互指specs/llm-defaults.md 的每条 [x] 都能映射到 4.1–4.2 的源码位置与第 5 节的测试用例,形成“规格 → 实现 → 测试”的三方闭环。

7. 小结

LLD-001 用一个看似简单的规则——“空值/空白值一律回退到前端默认模型,且加密路径同样适用”——解决了 OpenHands 前端与 agent-server 之间“默认模型不一致”的隐患。其工程价值体现在三点:默认值集中在 DEFAULT_SETTINGS.llm_model 单一常量;回退逻辑作为纯函数内嵌于 buildStartConversationRequest 构建链,明文与加密两条设置来源共享同一实现;参数化测试覆盖了空串、纯空白、无 llm 块、整体为空四种形态及加密载荷的对应形态。这套“规格文档 + 单一事实源 + 参数化测试”的组合,也为仓库内其他默认值类行为(如 config/defaults.json 相关配置)提供了可参照的实现范式。

登录后查看全文
热门项目推荐
相关项目推荐

项目优选

收起
kernelkernel
deepin linux kernel
C
33
18
ops-transformerops-transformer
本项目是CANN提供的transformer类大模型算子库,实现网络在NPU上加速计算。
C++
1.12 K
2.72 K
kernelkernel
openEuler内核是openEuler操作系统的核心,既是系统性能与稳定性的基石,也是连接处理器、设备与服务的桥梁。
C
527
590
ops-nnops-nn
本项目是CANN提供的神经网络类计算算子库,实现网络在NPU上加速计算。
C++
904
1.82 K
pytorchpytorch
作为 Ascend for PyTorch 社区的核心组件,TorchNPU 是昇腾专为 PyTorch 打造的深度学习适配插件,使 PyTorch 框架能够直接调用昇腾 NPU,为开发者提供昇腾 AI 处理器的超强算力。
Python
854
1.34 K
docsdocs
暂无描述
Markdown
889
5.78 K
jiuwenswarmjiuwenswarm
JiuwenSwarm 是一款基于openJiuwen开发的智能AI Agent,它能够将大语言模型的强大能力,通过你日常使用的各类通讯应用,直接延伸至你的指尖。
Python
3.52 K
1.01 K
ops-mathops-math
本项目是CANN提供的数学类基础计算算子库,实现网络在NPU上加速计算。
C++
1.33 K
1.45 K
cann-learning-hubcann-learning-hub
CANN 学习中心仓,支持在线互动运行、边学边练,提供教程、示例与优化方案,一站式助力昇腾开发者快速上手。
Jupyter Notebook
980
502
AscendNPU-IRAscendNPU-IR
AscendNPU-IR是基于MLIR(Multi-Level Intermediate Representation)构建的,面向昇腾亲和算子编译时使用的中间表示,提供昇腾完备表达能力,通过编译优化提升昇腾AI处理器计算效率,支持通过生态框架使能昇腾AI处理器与深度调优
C++
540
384