
字节笔记本
2026年10月6日 · 约 7 分钟读完
DeepSeek Harness 用户提问接口设计解析
Agent 在执行长任务时并不能总能自己拿主意:这条命令要不要放行、这份计划能不能执行、几个候选方案选哪个,往往需要人来拍板。麻烦在于,框架层如果直接依赖某个具体 UI,「向人提问」就会被绑死在某一种前端上。DeepSeek Harness(DeepSeek 开源的 agent 框架,MIT 协议,基于 Cordis 插件体系)把这件事做成了独立的一条交互接缝:packages/interaction/user-questions 包定义了一套提供方无关的提问词汇。工具或权限插件只管发问,UI 侧提供活跃的 UserQuestionProvider,host 运行时负责把请求转发给它连接的客户端。

问题条目:稳定 id 撑起批量提问
一次提问可以携带一组问题。AskUserQuestionItem 是其中的一个问题,调用方为它提供稳定的 id,回答会把这个 id 原样带回。于是相关的一批问题可以在一个界面流程里连续呈现,回答仍能逐条路由回各自的问题。除题面 question 外,条目还支持可选的 detail 辅助说明和 header 分组标签;options 给出可渲染成菜单的候选项,multiSelect 打开多选,默认单选。
一个容易被略过的细节是 detail 的归属:提供方会把它随问题一起渲染,但不会混进选项标签里。题面、辅助文本、选项标签三种文本各归各位,UI 做精致呈现时有料可用,模型读回答时也不会被辅助文案污染。
interface AskUserQuestionItem {
id: string // 稳定 id,随答案原样返回
question: string // 题面
detail?: string // 辅助文本,随题渲染,不进选项标签
header?: string // 可选分组标签
options?: AskUserQuestionOption[]
multiSelect?: boolean // 默认单选
intent?: AskUserQuestionIntent
}选项与回答:label 一词两用,custom 兜住自由文本
选项结构刻意保持极简:label 既是给人看的按钮文字,也是回传给模型的选中值;可选的 description 供能力更强的 UI 渲染补充说明。显示值与提交值合用一个字段,省掉了两者之间的转换层,也保证 UI 无论怎么渲染,模型读到的字符串不会走样。
回答侧的规则把三种情形都安排清楚了。selected 装选中的选项标签;custom 装用户自由输入的「其他」回答。单选题里 custom 一旦出现就覆盖选项,此时 selected 为空;多选题里 custom 与 selected 可以并存,作为对已选标签的补充。还有一个容易忽略的语义:selected 为空且没有 custom 的回答项是合法的,UI 用它在已完成的批次里标记被跳过的问题,其余问题的回答照常返回。
呈现意图:只改呈现,不改协议
AskUserQuestionIntent 是这套设计里最巧的一层。它允许调用方声明这个问题本质上是某种已知的决策类型,目前定义了 plan-review 一种:detail 必须携带计划 Markdown,这是 ask() 的硬性要求;approve 字段指名哪一个选项是肯定项,其余选项都算否决。
这里有两个值得记住的取舍。其一,approve 用名字指认肯定项,而不是靠选项位置约定,任何 UI 都不会从顺序里猜结论。其二,意图只改变呈现方式,不改变协议:认识 plan-review 的 UI 可以把问题渲染成计划审批卡片,不认识某个新标签的 UI 回退到通用选项列表,而两种情况下回答的编码完全一致,调用方读到的字段没有任何差别。按 kind 打标签的设计也给后续新增意图留了口子。
类型系统管不住的两种非法组合由 ask() 在运行时拒绝:approve 没有指向该问题自身的任何选项,以及给没有 detail 的问题指定意图。
单一提供方,effect 兜底生命周期
同一上下文里只允许一个活跃的提问提供方。UI 侧通过 ctx.userQuestions.registerProvider() 注册实现,调用返回一个 disposer 用于注销。注册绑定在 effect 上,热模块替换或资源释放时,当前活跃的 UI 提供方会被自动移除。把「谁来回答问题」的唯一性交给框架的组件生命周期管理,而不是靠开发者自觉,悬空引用的问题就此消失。
谁有资格向人提问:运行时根才有答案
这是整条接缝里最值得细读的边界。请求里可以带 agent 字段,但它必须是存活的那一个调用方实例,而且只有当前注册表把该实例识别为运行时根时,提问才会被接纳。判断依据是运行时归属关系,不是会话血缘:被拥有的子 agent 没有人类回答者,让它提问只会永远阻塞;带着血缘的会话在新运行时根上恢复后,则可以正常提问。
不合格的调用会得到两个明确的错误码:CALLER_NOT_LIVE 表示提供的 agent 不是注册表中的存活实例,DELEGATED_CALLER 表示这个存活的 agent 本身被其他 agent 拥有。没有这道闸门,一个后台委派任务可能悄无声息地挂死在等人回答上。
面向模型的错误分类
UserQuestionError 继承框架的 HarnessError,经 ctx.tools.execute() 抛出时保留 name 和 code,模型读得到结构化的失败原因:EMPTY_QUESTIONS 表示问题列表为空,NO_PROVIDER 表示没有活跃 UI,ASK_ABORTED 表示请求被中止,此外还有 UI 侧取消。错误码面向模型设计,意味着 agent 收到失败后能自己判断该重试、该换路径还是该把情况报告给用户。

小结
这套接口没有一处依赖具体 UI 框架,却把人机交互里最容易含糊的地方全部钉死:问题靠稳定 id 路由,回答靠 selected 与 custom 的组合消除歧义,呈现与协议用意图标签分层,提问资格用运行时归属把关,提供方生命周期交给 effect。正在做 agent 框架、或者要给 agent 加人工确认环节的开发者,可以直接把这组设计当参考实现来读。项目目前处于开发者预览阶段,接口仍可能调整;源码在 GitHub 的 deepseek-ai/deepseek-harness 仓库,MIT 协议。



