源码追踪|揭开 DSH 提示词的神秘面纱

发布时间:2026/9/2 16:10:06
源码追踪|揭开 DSH 提示词的神秘面纱 博主最近在挖 DSH 源码的时候发现 prompt 字符串到处都是事件类型一大堆可就是串不起来——模型到底什么时候看到了什么工具调用又是怎么绕一圈回到程序的单看某一处代码好像都认识但拼在一起就成了一团迷雾。本文从用户发一句话开始详细讲解DSH里面的提示词的完整流向。1. 分清五种数据在同一个“用户提问 → 模型回答 → 工具执行”的过程中DSH 会使用五种不同的数据。理解它们是读懂源码的关键所在。最容易混淆的是后两项assistant/chunk用于保存流式输出的原始增量assistant/message则是这些增量组装完成后的完整助手消息真正作为后续对话历史发给模型的是后者。工具调用也是如此日志中保留原始参数字符串的tool/call而下一轮请求中模型看到的是包含tool-result块的结果消息。2. 一张完整的数据流图假设用户说“读取README.md告诉我项目是做什么的”一次 turn 用户任务会经过下面的路径。注意turn是用户一次请求的完整处理过程step是其中“一次模型请求加上该次模型请求要求的所有工具调用”。模型只返回文本时一个 turn 通常只有一个 step模型先调用read、拿到文件内容、再回答时则是同一个 turn 中的两个 step。3. 流程详解3.1 用户输入之后程序先做什么前端或 SDK 会把用户文本包装成UserMessage并通过agent.followup()放进 Agent 的 inbox。它此时还不是永久对话历史Agent Loop 会先打开turn/start领取本轮输入并运行agent/pre-stepwaterfall这个扩展点可以拒绝输入、改写输入或补充一次性的上下文。在pre-step之前Agent Loop 会调用ctx.systemPrompt.assemble()所有已挂载插件在这里贡献本轮的section、context、变量以及可见工具 schema。运行时上下文会被做成一条额外的 user-role 快照消息只有快照与上一次不同才会加入避免每一步重复一整段时间、沙箱或审批信息。pre-step接受后Agent Loop 先写入step/start再把用户输入和这条动态快照依次追加为user/message事件。这就是“模型可见的信息必须被记录”的实际落点不是从内存里悄悄塞一段 prompt而是先记入 Session再由 Session 统一投影给模型。3.2 这一刻实际传给模型的内容是什么Agent Loop 用session.deriveMessages()从 Session 的模型可见事件中重建历史并和刚组装的系统提示词、工具 schema 一起形成GenerateOptions。它的抽象结构如下system、messages、tools是三条独立通道不要把它们都理解成一大段 system prompt。{provider:deepseek,model:...,system:身份、persona、工具使用规则等 section 拼出的文本,messages:[{role:user,content:[{type:text,text:读取 README.md告诉我项目是做什么的。}]},{role:user,content:[{type:text,text:Current runtime context. ...}]},],tools:[{name:read,description:Read a UTF-8 text file and return line-numbered content.,parameters:{type:object,properties:{file_path:{type:string}},required:[file_path]},},],sessionId:...,signal:AbortSignal,}上面的 TypeScript 是 Harness 内部的提供方无关表示。dsh-llm-deepseek会把system放到 DeepSeekmessages数组的第一条{ role: system }把普通历史转换为{ role: user }或{ role: assistant }把工具 schema 转成 DeepSeek 的tools: [{ type: function, function: { name, description, parameters } }]请求还会带上stream: true和stream_options: { include_usage: true }。稳定规则通常进入system当前状态成为一条用户消息工具能力主要进入tools而工具、计划、子 Agent 等模块也可能同时贡献这几种内容。3.3 模型返回的工具调用程序怎样解析 DeepSeek 不会返回 JavaScript 函数调用它以 SSE 持续返回 JSON 增量当模型选择工具时增量位于choices[].delta.tool_calls其中包含调用 id、函数名和分段到达的function.arguments字符串。例如完整拼合后可能是{id:call_read_123,type:function,function:{name:read,arguments:{\file_path\:\README.md\}}}这里有两层转换不能跳过。packages/llm/llm-deepseek/src/translate.ts读取 SSE分别把文字、推理内容和每一个工具调用翻译成统一的StreamChunk例如text-delta、reasoning-delta、tool-call-delta、block-end和最终的finish。Agent Loop一边把每个StreamChunk记录为assistant/chunk一边交给BlockAssembler流结束后BlockAssembler按 block index 拼回完整的text、reasoning和tool-call块生成一条assistant/message。于是上例在 Harness 内部变成{type:tool-call,id:call_read_123,name:read,arguments:{file_path:README.md},}arguments在这一刻仍是模型返回的原始字符串这样 Session 能精确回放模型究竟请求了什么。真正准备执行时调度器才尝试JSON.parse()空字符串按{}处理非法 JSON 会以原字符串继续进入工具的参数校验最终成为可返回给模型的错误结果而不会把解析异常直接当作宿主程序崩溃。3.4 工具不是模型直接执行的它还要经过一条管线Agent Loop 从assistant/message中筛出全部tool-call块按模型给出的调用顺序写入tool/call事件。可并行的工具可以同时运行但结果和额外上下文仍按模型调用顺序提交所以后续历史不会因为完成时间不同而乱序。每个调用交给ctx.tools.execute()后会经过以下步骤ToolRuntime复制并冻结参数解析当前 Agent scope 下可见的工具定义。tools/pre-execute执行策略。它可以允许、拒绝或要求用户审批之后还会执行不能被前置插件放宽的 guard。tools/executewaterfall 调用真正的工具实现。例如read调用文件系统 providerbash通过 shell/subprocess provider 启动命令web_search调用 Web provider工具本身只依赖它所属的能力接口因此本地实现和远程沙箱实现可以互换。对由defineTool()定义的工具工具实现前会按它发送给模型的同一份 JSON Schema 校验参数无效参数会变成INVALID_ARGS错误结果。成功返回值也会按输出 schema 校验并渲染为模型可读的ContentBlock[]。tools/post-execute可以接受、替换、补充或屏蔽结果最后工具定义的finalizeContent和tools/result观察者处理最终结果。最后Agent Loop 写入两类事件tool/call保存工具名和原始参数tool/result保存带同一callId的模型可读结果后者会投影为内部的 user-roleToolResultMessage{role:user,content:[{type:tool-result,toolCallId:call_read_123,content:[{type:text,text:1: # DeepSeek Harness\\n...}],isError:false,}],}DeepSeek adapter 再把它转换为 wire 上的{ role: tool, tool_call_id: call_read_123, content: ... }这条消息必须紧跟产生该 id 的助手工具调用消息模型才能把结果与请求对应起来。3.5 为什么工具结果会让模型再回答一次工具执行完不代表 turn 结束。Agent Loop 会开始同一 turn 的下一 step重新组装本轮的 prompt 和当前可见工具再从 Session 导出历史。这份历史已经包含原用户问题、模型刚才的tool-call、以及tool-result模型因此能读到README.md的内容并输出“这个项目是……”。如果这次响应没有tool-call块Agent Loop 记录最终assistant/message依次写入step/end和turn/endWeb UI 或 SDK 从 Session 事件中显示答案。换句话说工具结果不是由程序替模型拼进最终答案而是作为下一次模型请求的输入模型仍然负责决定如何解释结果、是否继续调用工具、以及怎样对用户作答。4. 文末总结回看整个过程DSH 的 prompt 流可以浓缩成一条主线一切可见内容必须先进 Session再被投影给模型。五种数据是理解源码的地图尤其是 assistant/chunk , assistant/message,tool/call 和 tool/result 的区别——前者是原始记录后者才是模型下一轮真正看到的内容。每个 step 都重新组装一次完整上下文系统提示词、运行时快照、历史消息、工具 schema 分别在 system、messages、tools 三条通道中传给模型而不是塞进一大段文本。工具调用是执行—写回—再请求的循环模型返回 tool-call 后程序执行并把结果写成带同一 callId 的 user-role 消息。下一轮请求时模型重新阅读完整历史自己决定如何解释结果。provider 边界只做翻译不改逻辑内部的 tool-result 消息和 DeepSeek 的 role: “tool” 是同一个东西的两种表示。如果在源码里遇到 prompt 相关代码时不妨从这三个角度去思考这样 DSH 的 prompt 流向就不再是迷雾这条消息写入 Session 了吗 它在下一轮 deriveMessages 时会被投影出来吗 provider adapter 会把它翻译成什么格式感谢大家的阅读希望能帮助到大家本文完