发布时间:2026/9/1 20:18:16
DeepSeek Harness 源码解读(一):从 dsh 入口追到第一次模型请求 第一次打开 DeepSeek Harness 仓库我做了一件很自然、但没什么用的事搜索main再找一个像AgentApp或AgentRuntime的总控类。没找到。packages/下面是 Agent、Session、Tools、LLM、Shell、Sandbox 等几十个包。每个名字都认识放在一起却很难看出程序究竟从哪里启动。沿着目录挨个读很快就会在接口、插件和配置文件之间迷路。后来我换了个办法不再猜哪个类最重要而是从一条真的能跑起来的命令往下追pnpmdsh--profileheadless列出这个仓库最值得先读的三个文件我只追一个问题这段文字经过哪些代码才变成第一次模型请求这条线不算长却会经过 CLI 分发、Profile 组合、Cordis 装载、服务注册、Agent 创建和 Session 记录。走完以后仓库里那些看起来分散的包就有了位置。项目地址deepseek-ai/deepseek-harness源码基线0.1.0-rc.5bin.ts没有启动 Agent入口在apps/cli/src/bin.ts。我原本以为这里至少会创建模型客户端实际代码只做命令解析和模式分发。删去另外两个分支后profile路径大致是这样constinvocationparseDshArgs(process.argv.slice(2),readVersion())switch(invocation.mode){caseprofile:{const{runProfile}awaitimport(./profile-boot.ts)awaitrunProfile({environment:loadLayeredEnv(dsh),profile:invocation.profile,patchFiles:invocation.patches,args:invocation.args,})break}// plugin、dump-config 等分支略}bin.ts能看懂--profile headless也能把后面的任务参数交给runProfile()但它不知道本次使用哪个模型、有哪些工具甚至不知道 Agent Loop 是否存在。这里的动态导入也很实在运行 Profile 时才加载profile-boot.ts执行插件管理或配置导出时不必顺手拉起整套启动逻辑。所以入口文件更像一个分诊台。它把命令送到对应流程真正的运行时还在后面。headless不是布尔开关接着看packages/boot/app-boot/src/profile.ts。仓库内置的 Profile 模板只有两组exportconstPROFILE_TEMPLATES:Recordstring,readonlystring[]{web:[deepseek-ai/dsh-base,deepseek-ai/dsh-web-app],headless:[deepseek-ai/dsh-base,deepseek-ai/dsh-headless],}headless并不是if (headless) ...这样的功能开关。它表示先装基础 Bundle再叠加 Headless Bundle。这里的 Bundle 也不是常说的前端打包产物。每个 Bundle 提供一层cordis.patch.yml往插件树里插入或修改配置。dsh-base放入模型、Session、Agent、工具等通用能力dsh-headless再补上一条命令行任务的直接驱动器。apps/cli/src/profile-boot.ts会按顺序合并这些层Bundle patches → 当前 Profile 的 cordis.patch.yml → $DSH_HOME/cordis.patch.yml → 命令行 --patch → 必要时追加 telemetry 禁用补丁后面的补丁可以覆盖前面的配置。也就是说源码里有某个插件不等于这次进程一定装了它同一个插件存在也不等于它使用仓库里的默认参数。读到这里我先记住了一个排查命令pnpmdsh--profileheadless --dump-config与其只翻packages/猜运行时不如先看这次启动最终合成了哪棵插件树。boot()先造一个空的 Context配置准备好以后流程进入packages/boot/app-boot/src/index.ts的boot()。把错误包装和退出检查暂时拿掉主干很短constctxnewContext()try{ctx.baseUrlpathToFileURL(dirname(absoluteConfigPath)).href/ctx.provide(dshHomePath,dshHomePath)awaitctx.plugin(Loader)awaitprepare?.(ctx)awaitmountRootInclude(ctx,absoluteConfigPath,patches,bareModuleBaseUrl)awaitctx.get(loader)?.await()awaitassertEntriesActivated(ctx,binName)returnctx}catch(cause){awaitctx.fiber.dispose()throwcause}这里最值得注意的不是 Loader而是没有出现一长串new没有new SessionStore()没有new ToolRuntime()也没有new AgentLoop()。boot()只先创建一个 CordisContext再让 Loader 按刚才合成的配置逐项挂载插件。Session、LLM、Tools 等服务由各自插件注册进 Context不由启动函数集中组装。这种写法起初比手工创建对象绕一点但生命周期很清楚。某个插件注册的服务、监听器和清理动作都挂在 Fiber 上启动中途失败时释放根 Fiber 就能沿插件树回收已经创建的资源。我把前半段调用链画成了一张图。后面的文章还会不断回到这张图上Agent Loop 是配置装进来的再打开packages/bundle/base/cordis.patch.yml才能看到核心服务怎样进入 Context-id:llmname:deepseek-ai/dsh-llm-id:sessionname:deepseek-ai/dsh-session-id:agentname:deepseek-ai/dsh-agent-id:toolsname:deepseek-ai/dsh-tools-id:system-promptname:deepseek-ai/dsh-system-prompt-id:agent-loopname:deepseek-ai/dsh-agent-loop这些条目不是文档目录而是启动时真正会被 Loader 处理的插件项。packages/core/agent-loop/src/index.ts里的AgentLoop明确写出了自己依赖的服务exportclassAgentLoopextendsServiceimplementsAgentFactory{staticinject[agents,sessions,llm,tools,systemPrompt]constructor(ctx:Context,config:Config){super(ctx,agentLoop)// 其他初始化略ctx.effect(()ctx.agents.setFactory(this),agentLoop.setFactory())}}这五个名字基本就是主干ctx.agents保存活动 Agent并提供统一的创建入口ctx.sessions管理追加式会话日志ctx.llm负责找到模型 Provider 并发起请求ctx.tools维护工具注册和执行管线ctx.systemPrompt组装每一步要交给模型的提示词与工具定义。AgentLoop把它们串起来但不把具体模型、存储实现或 Shell 工具写死在自己内部。构造函数中的setFactory(this)才是关键交接Agent Registry 仍是公共入口当前装载的 Agent Loop 成为它背后的创建工厂。如果只看类名很容易把AgentLoop当成整个程序放回插件树里看它只是其中一个协调服务。Headless Runner 只负责投递任务基础能力都装好后packages/bundle/headless/src/index.ts的 Runner 才开始处理命令行里的那段文字。省去模型选择和输出汇总主要动作是这些const{agent}awaitagents.create({sessionId:SessionId(session-${randomUUID()}),meta:{cwd:process.cwd()},agentOptions:{provider:selection.provider,model:selection.model,},})awaitagent.whenIdle()agent.followup(createUserMessage({content:[{type:text,text:task}],source:{kind:user},}))awaitagent.whenIdle()awaitsessions.flush(agent.session)Runner 没有把task直接塞给某个模型 SDK。它先通过ctx.agents创建 Agent再把命令行文本包装成普通用户消息用followup()投进 Agent 的 Inbox。两个whenIdle()也不是多余的。第一个等 Agent 初始化完成第二个等这次任务把 Agent 跑回空闲随后sessions.flush()确保会话落盘Runner 才从事件中整理最终文本并决定退出码。这段代码解释了为什么 Web、Headless、ACP 和 SDK 不需要各写一套推理循环。它们接收输入的方式不同进入运行时以后走的仍是同一个 Agent 接口。第一次模型请求藏在 Step 里任务进入 Inbox 后Agent 会打开 Turn再进入 Step。真正的模型调用在packages/core/agent-loop/src/agent.tsconst{request,preparedCall}awaitthis.buildRequest(turn,step,assembly.tools,system,this.session.deriveMessages(),signal,)constassemblernewBlockAssembler()constchunkSeqs:number[][]conststreampreparedCall?.stream(request)??this.loopCtx.llm.stream(request)forawait(constchunkofstream){chunkSeqs.push(this.session.append(assistant/chunk,{turn,step,chunk}).seq,)assembler.push(chunk)}走到stream(request)这条命令才第一次碰到模型。请求也不是临时拼一段字符串历史消息来自session.deriveMessages()工具定义来自本 Step 的 Prompt 组装结果模型返回的流式分片则逐个写成assistant/chunk。分片收齐后还会生成一条assistant/message作为后续回放和下一步推理使用的正式消息。如果这条消息只有文本当前 Step 可以结束如果里面带着 tool callAgent Loop 会执行工具、记录tool/call和tool/result再开下一个 Step 请求模型继续回答。所以“一次命令行任务”不等于“一次模型请求”。一次 Turn 里可以有多个 Step每个 Step 才对应一次模型交互。这里先停住后面再单独拆 Turn、Step 和工具循环。走完以后仓库里仍然没有“主程序”这条调用链可以压成下面几步apps/cli/src/bin.ts → apps/cli/src/profile-boot.ts → packages/boot/app-boot/src/index.ts → packages/bundle/base/cordis.patch.yml → packages/bundle/headless/src/index.ts → packages/core/agent-loop/src/agent.ts我最后还是没找到一个包办所有事情的AgentApp因为这套代码本来就没有把运行时收进一个大对象。CLI 选择启动方式Profile 决定插件组合Cordis 管理装载和生命周期Agent Loop 协调服务Session 保存可恢复的事实具体模型和工具则从各自的 Provider 进入。所谓 Harness不是给模型 API 再包一层方法而是把这些原本散落在外面的运行条件组织起来。如果只做一次问答直接调用模型当然更短。可一旦要恢复会话、切换 Provider、挂载工具、处理审批与沙箱还要让 Web 和 CLI 共用同一套执行逻辑真正麻烦的就不再是那一行llm.stream()。下一篇从new Context()这里接着读Cordis 的 Service、Fiber、Effect 和waterfall究竟怎样把这棵插件树托起来。

相关新闻

2026/9/1 20:13:15

Python爬虫与数据可视化实战:天气轮播图期末大作业完整案例

这次我们来看一个特别适合 Python 期末大作业的完整项目:用 Python 爬取天气数据,再把结果做成数据可视化图表,最后以「天气轮播图」的形式在网页上展示。项目本身不复杂,但覆盖了数据采集、数据清洗、数据存储、可视化、Web 展示…

2026/9/1 20:13:15

Python金融统计实战:Pandas数据处理与回归策略回测

港科大这套《Python金融统计:Pandas到回归策略完整课》,核心就是一条主线:用 Python 处理金融数据,再用统计模型做回归分析,最后把回归结果落到一个可回测的策略框架里。它不是单纯讲 Pandas API,也不是单独…

2026/9/1 20:13:15

Malody乱力进阶:用replay复盘把96.14变成稳定输出的起点

打完 Extra-4 的 Pure Ruby,屏幕上跳出 96.14 的时候,我做的第一件事不是立刻换下一张谱,而是先把 replay 保存下来。很多玩 Malody 乱力进阶的人到这个阶段都会有类似的体感:成绩已经过了 95,但回看过程时总觉得有一部…

2026/9/1 20:33:18

美团校招笔试复盘:从贪心到状态压缩的算法突破

1. 第2场笔试给我的整体印象:时间紧、样例弱、边界多1.1 试卷结构与时长带来的压迫感我参加的是美团2023校招技术岗的第2场在线笔试,整场下来最大的感受就是:题量不算夸张,但时间分配稍有不慎就会翻车。编程题部分通常是4道&#…

2026/9/1 20:33:18

技术博客创作必备:从项目资料到关键词布局的完整指南

当前为测试会话,未提供具体技术项目标题与可用的项目正文材料。无法生成“可直接发布的 CSDN 技术长文”。请按以下格式补充真实可用的项目资料:项目标题:例如“xxx 开源本地部署工具” 项目正文:可包含项目简介、功能列表、安装命…

2026/9/1 20:33:18

Odoo ERP在印刷行业文档管理中的落地实践与部署方案

这次我们来看一个实战向的话题:Odoo ERP 在印刷行业的文档管理。印刷企业的数字化转型里,文档管理往往比排产系统更早被提上日程,客户来稿、打样确认、工艺单、版材记录、质检报告、外发回单,这些文件如果继续放在共享盘里&#x…

2026/9/1 20:33:18

用一条河讲透Flink核心概念:流处理、窗口与状态

很多刚接触 Flink 的开发者,最大的障碍往往不是 API 本身,而是被一堆抽象术语劝退:无界流、有界流、窗口、水位线、状态后端、检查点、精确一次……每个词单独看都能理解,合在一起就不知道它们在系统里到底扮演什么角色。这篇文章…

2026/9/1 20:28:17

Python生成器与迭代器:高效处理大数据

Python生成器与迭代器:高效处理大数据 生成器和迭代器是Python处理大数据和流式数据的核心工具,能节省内存、提高效率。本篇将深入学习迭代器协议、生成器函数和生成器表达式。 一、迭代器(Iterator) 1. 什么是迭代器? 迭代器是实现了迭代器协议的对象,即包含 __iter__…

2026/9/1 16:02:17

vSound小提琴数字处理器实操指南:从接线到演出的完整配置

电小提琴或者原声小提琴插电演出,第一个绕不开的坎就是声音难听。原声琴的共鸣和空气感一旦进了拾音器,出来的往往是一坨干瘪、发尖、带着奇怪塑料味的信号。我当初第一次把琴接上乐队调音台,直接被主唱吐槽"你这声音像在锯钢丝"。…

2026/9/1 8:27:47

传感器接口IC如何攻克生物化学传感的微弱信号难题?

1. 从电极到比特流:为什么生物化学传感必须依赖专用接口IC 做生物化学传感的人都有过类似的经历:明明传感器本身性能很好,信号输出却一塌糊涂——噪声大、漂移明显、重复性差,怎么调都达不到预期。很多时候问题并不在传感器&#…

2026/9/1 7:04:43

STM32F411CEU6多通道ADC采集:扫描模式+DMA实现详解

1. 多通道 ADC 的用武之地把“Multichannel ADC”和“STM32F411CEU6”这两个关键字放在一起,其实就是嵌入式开发里最常遇到的一类需求:用一块不算贵的 MCU,同时采集多路模拟信号。STM32F411CEU6 是 48 引脚的 Cortex-M4F 主控,主频…

2026/9/1 0:00:42

USB Type-C PCB布局分区设计:电源、高速信号与PD协议全攻略

做硬件这行,Type-C接口算是典型的“看着简单,做起来全坑”的东西。光引脚就24个,高低速信号、电源、控制线全部塞在一个小小的连接器里,如果PCB布局不做规划,打样回来基本就是“插上没反应”、“高速掉线”、“静电一打…

2026/9/1 0:00:42

系统编程学习原型如何补齐稳定性边界

系统编程学习原型如何补齐稳定性边界预算有限时&#xff0c;我先优化明显多余的复制&#xff0c;而不是猜测性地换容器。用借用传递只读数据通常就能减少分配&#xff1a; fn parse(line: &str) -> Result<Item, Error> { /* ... */ }用基准确认热点确实在分配&am…

2026/9/1 0:00:42

雨花区哪家财务公司代理记账比较好?

在雨花区&#xff0c;企业处理财税事务常常面临诸多挑战&#xff0c;选择一家靠谱的财务公司至关重要。湖南巨勤财务管理咨询有限公司就是本地正规实体财税服务机构&#xff0c;深耕本地工商财税行业多年&#xff0c;熟悉当地工商局、税务局最新政策与申报流程。主营公司注册、…

2026/9/1 0:00:42

USB Type-C PCB布局分区设计:电源、高速信号与PD协议全攻略

做硬件这行&#xff0c;Type-C接口算是典型的“看着简单&#xff0c;做起来全坑”的东西。光引脚就24个&#xff0c;高低速信号、电源、控制线全部塞在一个小小的连接器里&#xff0c;如果PCB布局不做规划&#xff0c;打样回来基本就是“插上没反应”、“高速掉线”、“静电一打…

2026/9/1 0:00:42

系统编程学习原型如何补齐稳定性边界

系统编程学习原型如何补齐稳定性边界预算有限时&#xff0c;我先优化明显多余的复制&#xff0c;而不是猜测性地换容器。用借用传递只读数据通常就能减少分配&#xff1a; fn parse(line: &str) -> Result<Item, Error> { /* ... */ }用基准确认热点确实在分配&am…

2026/9/1 0:00:42

雨花区哪家财务公司代理记账比较好?

在雨花区&#xff0c;企业处理财税事务常常面临诸多挑战&#xff0c;选择一家靠谱的财务公司至关重要。湖南巨勤财务管理咨询有限公司就是本地正规实体财税服务机构&#xff0c;深耕本地工商财税行业多年&#xff0c;熟悉当地工商局、税务局最新政策与申报流程。主营公司注册、…