DeepSeek Harness 上下文拼接:让 AI 准确行动

vbnet 复制代码
title: "DeepSeek Harness 上下文拼接:让 AI 准确行动"
subtitle: "模型看到的上下文是三层拼出来的:系统提示词 + 规则记忆 + 动态快照"
description: "模型每次请求看到什么?不只是你发的那条消息------还有系统提示词、项目规则(AGENTS.md)、动态快照,它们一起拼成模型眼前的上下文。DeepSeek Harness 把上下文拼成三层:Sections(系统提示词,System 角色)、Agent Instructions(规则与记忆,User 角色)、Contexts(动态快照,User 角色)。本文讲清三层各自的内容、拼装流程(preStep 三步)、以及最终消息顺序。"
summary: "拆解 DeepSeek Harness 的上下文拼接:Sections / Agent Instructions / Contexts 三层结构、preStep 拼接流程、最终消息顺序。"
keywords:
  - DeepSeek Harness
  - 上下文拼接
  - System Prompt
  - Agent Instructions
  - AI Agent 框架
tags:
  - 源码解析
  - AI Agent
  - TypeScript
categories:
  - 深入理解 DeepSeek Harness
slug: "deepseek-harness-context-assembly"
series:
  - "深入理解 DeepSeek Harness"
series_weight: 2
toc: true

DeepSeek Harness 上下文拼接:让 AI 准确行动

「深入理解 DeepSeek Harness」系列第 2 篇。全系列地图见第 0 篇《5 分钟看懂 DeepSeek Harness》

引言

模型每次请求,看到的不是只有你发的那条消息。

它还看到:系统提示词("你是资深架构师")、项目规则(AGENTS.md 里写的"改代码先读测试")、记忆、当前打开的文件的快照------这些全部拼在一起,才是模型眼前的"上下文"。而上下文长什么样,直接决定模型的表现。

问题来了:这段上下文从哪来?把上下文拆成几段拼起来,是业内共识。真正的差别在谁在拼、怎么拼------多数框架的拼装逻辑写死在框架内部,加一段内容就要改框架代码。

DeepSeek Harness 把上下文拆成三层,各自由不同的插件贡献:

角色 内容 谁贡献
Sections System 系统提示词:身份、性格、工具引导 工具插件、身份插件
Agent Instructions User 规则与记忆:AGENTS.mdCLAUDE.md agent-instructions 插件
动态上下文(Context) User 动态快照:委托状态、沙箱策略、审批状态 context 插件

模型看到的上下文是三层拼出来的,不是哪一层单独写出来的

阅读门槛

前置知识 :需要 TypeScript 基础(能读懂 abstract classgenericsdeclaration merging),了解 ReAct Agent 基本概念(LLM 交替推理与行动的循环模式)。

适合:想要阅读 DeepSeek Harness 源码、做 Agent 框架二次开发的开发者。

不适合:零基础想学习大模型 Agent 入门的读者------建议先了解 ReAct 模式和 TypeScript 插件架构再回来。

下面按"三层各是什么 → 每层怎么拼 → 完整拼接流程 → 最终消息顺序"拆。

一图总览

图 1:三层内容汇入 preStep,拼成模型请求的两个字段------system(静态前缀)和 messages(用户输入、指令、快照、历史)。实线 = 贡献,虚线 = 拼装结果。

一、三层结构------解决"模型请求的上下文从哪来"

先分清两个东西:system-prompt 是模块名SystemPrompt 服务------注册 Sections 的注册表,负责收集、排序、渲染),不是拼好的提示词 。拼好的结果叫 system 文本 ,是模型请求里 system 字段那段文字。打个比方:system-prompt 是"拼图板 + 拼装工人",system 文本是"拼好的图"。下面讲的三层,就是往拼图板上放的三类拼图块。

Sections(系统提示词,System 角色) 。静态的系统级指令,三类:身份(真实文本是 "You are an AI agent powered by DeepSeek Harness.")、性格(persona,部署配置)、工具引导(比如 tool:read 的 "Use the read tool --- not shell commands like cat --- to inspect text files.")。按 order 排序拼成一段 system 文本------这是模型的稳定前缀,每次请求一样,命中 LLM 缓存。每类的真实例子见第二节。

Agent Instructions(代理指令,User 角色) 。项目级规则和记忆:AGENTS.mdCLAUDE.md、工作区规则。它动态加载 ------每次请求前从磁盘读,工具改动这些文件时自动重载。放 User 角色而不是 System,因为它不能污染稳定前缀:规则文件会变,塞进 system 会让缓存全部失效。

动态上下文(Context,User 角色) ------源码里叫 dynamic context。动态生成的运行时状态快照:子代理委托上下文、沙箱策略状态、用户审批状态。放 User 角色,变化才注入,被替换时打"已清除"标记。

三层分工一句话:System 管"你是谁、你能做什么",User 管"项目规则、当前状态"

二、Sections:系统提示词怎么拼

Section 是什么?

在 DeepSeek Harness 里,系统提示词不是一大段写死的字符串,而是由很多小块拼起来的。每个小块就叫 Section------一段有名字、有优先级、有内容的提示词片段。

接口定义长这样(system-prompt/index.ts:53):

typescript 复制代码
export interface PromptSection {
  /** 唯一名字------重复注册会报错(比如 tool:read 只能有一个)。 */
  readonly name: string
  /** 优先级,按升序拼接。约定:-100 是身份,0 是性格,100-199 是工具引导。 */
  readonly order: number
  /** 内容:静态文本,或每次拼装时求值的函数(可引用 {{variable}})。 */
  readonly text: string | ((context: AssembleContext) => string)
}

三个字段各管一件事:

  • name:唯一标识,同一作用域内重复注册会直接报错。
  • order:拼接顺序,数字越小越靠前。约定是 -100 身份,0 性格,100-199 工具引导。
  • text :可以是固定文本,也可以是每次拼装时才求值的函数,支持 {{variable}} 变量插值。

一句话:Section = 一段有名字、有优先级、有内容的提示词片段。系统提示词就是所有 Section 按 order 拼起来的结果。

完整 Sections 列表

当前代码库里注册的系统提示词 Section 如下,按 order 排序:

order name 内容
-100 harness:identity DeepSeek Harness 身份(框架注册)
0 deployment:persona 部署角色(框架注册占位,配置填内容)
100 tool:read Read 工具使用指导
101 tool:write Write 工具使用指导
102 tool:edit Edit 工具使用指导
103 tool:glob Glob 文件搜索指导
104 tool:grep Grep 内容搜索指导
105 tool:bash / tool:pwsh Shell 命令执行指导(按平台二选一)
106 tool:jobs / tool:pty 后台任务 / 终端会话指导
110 tool:web_search 网页搜索指导
111 tool:web_fetch 网页获取指导
112 tool:lsp LSP 语言服务器指导
113 tool:session-query 会话查询指导
114 tool:goal 目标管理指导
115 tool:cordis 运行时自修改指导
116 tool:ralph Ralph 迭代循环指导
(其他) tool:report 各工具插件的引导

框架本身只注册前两个:harness:identitydeployment:persona 占位。其余全是工具插件在 apply() 里动态注册的------注册即挂载,卸载即清理。

真实的提示词长这样

三段提示词对应三类:身份、性格、工具引导。工具引导举一个 tool:read 为例,其他工具同理:

bash 复制代码
# 身份:harness:identity(-100,框架注册)
You are an AI agent powered by DeepSeek Harness.
​
# 性格:deployment:persona(0,配置填入,这里假设部署配置)
你是资深架构师,说话直接,先看代码再下结论。
​
# 工具引导:tool:read(100,tool-fs 注册)
Use the read tool --- not shell commands like cat --- to inspect text files.
Results include line numbers. Use offset and limit to continue reading large files.

工具引导 Section 干的事很单纯:每个工具一句话教模型怎么用。一百个工具就一百句话,全排在 100-116 这个区间里,按 order 依次排开。

身份从哪来?

identity 是模型读到的第一句话------它声明"这个 AI 是什么驱动的"。真实文本就是你在例子里看到的那句:

csharp 复制代码
You are an AI agent powered by DeepSeek Harness.

它由框架注册:SystemPrompt 构造器里注册 harness:identity(order -100,system-prompt/index.ts:359)------不是插件放上来的,是框架自带的。内容固定,但可以配置关闭(includeHarnessIdentity,默认开启)。

身份回答的是"你是什么"------模型开口前先知道自己的出身。至于"你是什么性格",那是下一节 persona 的事。

persona 从哪来?

persona 不是写死在代码里的,而是配置项。在 cordis.patch.yml 里给 system-prompt 插件配一下:

yaml 复制代码
- id: system-prompt
  config:
    persona: '你是资深架构师,说话直接,先看代码再下结论。'

实现上,SystemPrompt 构造器会注册一个名为 deployment:persona 的占位 Section(order: 0),文本就是 config.persona。没配就渲染为空,这个位置不出现。

想给某个特定 Agent 换性格?注册同名 Section deployment:persona 就行,子作用域覆盖全局------同一个 order 0 位置,谁后注册谁生效。所以 persona 有三层来源:默认空 → 部署配置(yaml)→ 插件覆盖。

工具引导从哪来?

工具引导 Section 不是框架注册的,是每个工具插件自己注册的 ------工具插件在 apply() 里注册工具的同时,顺手注册引导:

vbnet 复制代码
// tool-fs 注册 read 工具时,顺手注册引导(tool-fs/src/read.ts)
ctx.systemPrompt.section({
  name: 'tool:read',
  order: 100,
  text: 'Use the read tool --- not shell commands like cat --- to inspect text files.',
})

顺序按 order 排在 100-199 区间(当前是 100-116),和身份、性格的先后关系就是:身份(-100)→ 性格(0)→ 工具引导(100+) ------模型先知道"你是什么",再知道"你什么性格",最后知道"你能用什么工具"。

工具插件注册的其实是两样东西,别混淆:引导 Section (教模型怎么用,散文,进 system 文本)+ 工具 schema(定义接口,JSON,进 tools 参数)。一个管"怎么用",一个管"长什么样",同一个插件一起注册、一起生效。

拼装流程

拼装是三步流水线(assemble()renderPrompt()):

  1. 收集:按作用域链合并所有 Section(子作用域同名覆盖全局),同时收集工具 schema 和变量。
  2. 排序:Section 按 order 升序;工具 schema 另按词法排序。
  3. 渲染:逐段处理------变量插值 → 丢弃空 Section → 空行连接成一段 system 文本。
scss 复制代码
sections
  .map(插值)          // 1. {{variable}} 替换成运行时值
  .filter(非空)       // 2. 空的 Section 直接丢掉
  .join(空行)         // 3. 剩下的用空行连起来

这套机制换来了什么?

  • 稳定前缀命中缓存:身份、性格、工具引导每次拼出的 system 文本一样,省 token 省延迟。
  • 扩展不碰核心:加一个工具 = 工具插件注册一个 Section,框架一行不用改。
  • 顺序自由:order 数字决定位置,插件想插哪就插哪。

系统提示词是插件们拼的拼图,框架只管拼装。

三、Agent Instructions:规则和记忆怎么加载

规则和记忆分两类进上下文:常驻规则AGENTS.md 等)每次请求都加载,按需技能(skill)用到才加载。先看常驻规则怎么被读到,再看 skill 什么时候加载、对它做了什么专门处理。

规则从哪来?AGENTS.md 怎么被读到

AI 凭什么守项目的规矩?因为每次请求前,agent-instructions 插件(packages/context/agent-instructions/)会把项目根的规则文件读进来。它在 agent/pre-step 事件里动手(index.ts 322 行),调用 loadBaselineInstructionSet(137 行)从磁盘读:

php 复制代码
// index.ts 137 行附近:加载基线指令集
const instructions = await loadBaselineInstructionSet({
  cwd,
  dshHome: resolved.dshHome,
  projectRootMarkers: resolved.projectRootMarkers,
  maxBytes: resolved.maxBytes,
  instructionFileCandidates: resolved.instructionFileCandidates,
  localInstructionFileCandidates: resolved.localInstructionFileCandidates,
  projectRoot,
  signal,
}, fileSystem)

读哪些文件?默认是这两组(config.ts 12-13 行):

csharp 复制代码
const DEFAULT_INSTRUCTION_FILE_CANDIDATES = ['AGENTS.md', 'CLAUDE.md'] as const
const DEFAULT_LOCAL_INSTRUCTION_FILE_CANDIDATES = ['AGENTS.local.md', 'CLAUDE.local.md'] as const

基线 (项目根的 AGENTS.md / CLAUDE.md)+ 本地 (子目录的 AGENTS.local.md / CLAUDE.local.md)。候选列表可扩展------部署时往 instructionFileCandidates 里加文件(比如 MEMORY.md 记忆索引),它就会被当成指令加载。注意默认并不包含 MEMORY.md,它属于可扩展项,不是开箱即用的。

规则改了怎么办?

工具执行完,agent-instructions 会检查这个工具是否触及了规则文件(read/write/edit 打开了 AGENTS.md?),触及了就标记,下一个 pre-step 重读(index.ts 350 行):

dart 复制代码
// index.ts 350 行附近:工具结果触发重新加载
ctx.on('tools/result', (exec, result) => {
  const touches = executionTouches.get(exec.token) ?? []
  // ... 从执行记录里找它读/写过的文件路径
  // 触及指令文件的 → 队列化投影,下一个 pre-step 重读
})

所以改规则不用重启------改完 AGENTS.md,下一步就生效。

规则怎么进消息?

pre-step 监听器在 waterfall 里调用 compose() 生成指令消息,然后折叠进 claimed 消息之后(源码注释原话:"Fold the context right after the claimed batch")------即指令作为 User 消息,紧跟用户输入。

什么时候加载?

常驻规则是"每次都要遵守的";skill 是"偶尔要用到的能力",按需加载

  • 目录常驻 :skill-filesystem 从项目、自定义、用户根目录发现技能,解析 YAML frontmatter,模型在上下文中只看到技能目录(catalog:名字 + 摘要);
  • 正文按需 :模型决定用某个技能时,调用 skill 工具,才把该技能的完整 SKILL.md 正文加载进上下文(正文通过 ctx.fs 按需读盘)。

对 skill 做了什么专门处理?

skill 不是简单的"读文件塞进去",有三层专门处理:

  1. frontmatter 解析 (skill-filesystem/index.ts 909 行 parseFrontmatter):SKILL.md 开头有 YAML frontmatter(name / description),解析出目录项(data)+ 正文(body)------模型看到的是解析后的目录(名字 + 摘要),不是整篇 skill,据此决定要不要调用;
  2. 目录与正文分离 :目录(catalog)是会话上下文的一部分,正文(SKILL.md body,body.trim() 清理后)在调用时才读盘------一百个技能只占一百行摘要,不占整篇正文;
  3. 监听更新watch 配置):技能目录变化(增删改)即时反映到目录,不用重启。

这套机制换来了什么?

常驻规则 + 按需技能的组合,换来的是执行效率:规则自动生效(AGENTS.md 每次请求自动加载,项目改规则 AI 下一步就跟上)、技能按需加载(一百个技能只占一百行摘要,只有用时才读正文------省 token、少干扰)、改动即时生效(工具触及规则文件自动重载、skill 目录 watch 监听,都不用重启)。一句话:常驻规则管"每次要守的规矩"(小、稳定、自动加载),按需技能管"偶尔要用的能力"(大、可选、省 token) ------各取所需,AI 在有限的上下文里只装最该装的。

四、动态上下文(Context):运行时快照怎么注入

动态上下文(源码里叫 dynamic context,system-prompt/index.ts 顶部注释)提供运行时状态快照------现在几点、终端什么状态、正在委托哪个子代理、沙箱允许什么、用户批没批准。它和 Sections 一样是内容提供者,区别是内容会变:Sections 管"你是谁"(静态、前缀、命中缓存),动态上下文管"你现在在哪"(动态、变化才注入)。注册单元是 PromptContext。下面看它谁提供的"变化才注入"怎么实现的

动态上下文从哪来?两种接入方式

动态上下文不是框架内置写死的,是各个 context 插件提供的------读代码时想知道"快照是谁提供的",就看它用哪种方式接入:

方式一:注册进 SystemPrompt。 插件在 apply() 里调 systemPrompt.context()(注册即挂载、卸载即清理),注册一个 PromptContextname / order / text,text 可以是每次拼装时求值的函数)。当前代码库里这类有三处:

name 谁注册 内容
subagent:delegation subagent(child-agent.ts 170 行) 子代理委托上下文
sandbox-policy sandbox-policy(113 行) 沙箱策略状态
user-approval user-approval(205 行) 用户审批状态

方式二:自己监听 agent/pre-step time-context(当前时间)、tmux-context(终端状态)、agent-instructions(规则)不走注册表,而是监听 agent/pre-step 事件(time-context 在 index.ts 170 行,{ prepend: true } 排在所有监听器最前),在 waterfall 里自己生成一条 User 消息、追加到 messages 尾部------比如 time-context 用自己的 refreshIntervalMs 节流,没到间隔就不重刷。

两种方式的区别一句话:方式一交给 SystemPrompt 统一收集渲染(assemble → project),方式二自己生成、自己追加 。读代码时:方式一的插件找 systemPrompt.context(),方式二的找 ctx.on('agent/pre-step')

真实的快照长这样

动态上下文提供的快照,有的注册时就是固定文本,有的每次拼装时现算(PromptContext.text 可以是函数)。两个真实例子:

sql 复制代码
# subagent:delegation(子代理委托,固定文本常量,child-agent.ts 135 行)
You are a delegated subagent: your permission scope was fixed when you were started
and cannot be widened from inside this session --- operations that require approval
are rejected automatically. When the task needs access beyond that scope, do not
retry the denied operation; state the limitation in your reply so the delegating
agent can handle it.
​
# time-context(时间,每次拼装现算,index.ts 110 行 renderText 的格式)
Time sampled while preparing turn 3, step 1: 2026-08-15 14:30:00 GMT+8
Elapsed since the preceding model-visible message: 2m 13s.

前者是常量(子代理的权限声明,每个子代理一样),后者是函数现算(时间每次不同)。sandbox-policyuser-approval 也是函数式的------读代码时它们的 text: (context) => ... 就是每次求值的入口。

"变化才注入"是怎么实现的?

注册方式的核心在 runtimeContext.project()(runtime-context.ts 64 行)------它记住上次注入的快照文本,每次渲染完比较:

kotlin 复制代码
project(current: string, sections) {
  // 空快照 → 打"已清除"标记,而不是静默消失
  const snapshot = current.length === 0 ? CLEARED : current
  // 和上次一样 → 不注入(返回 undefined)
  if (this.retained?.text === snapshot) return
  // 不一样 → 创建一条带来源的快照消息注入
  return { content: [{ type: 'text', text: snapshot }], ... }
}

三个分支对应三种情况:

  • 内容没变 → 不注入,省 token(这是"变化才注入"的实现);
  • 内容变成空 → 注入 CLEARED 标记("Current runtime context: none. Earlier runtime-context snapshots no longer apply.")------告诉模型"之前的快照不再适用",而不是默默消失;
  • 内容变了 → 注入新快照,消息带来源信息(哪几个 context 贡献的)。

自监听的方式没有这套文本比较------它用自己的节流(time-context 的 refreshIntervalMs),没到间隔不重刷。两种方式殊途同归:内容不变就不占上下文

五、拼接流程:三层内容怎么汇成一个请求

在哪个环节拼?preStep 四步

三层内容在 preStep(agent.ts 225-243)汇合------每次调模型前的准备,四步:

arduino 复制代码
private async preStep(target: InboxTarget, position: { turn, step }): Promise<PreparedStep> {
  const claimed = this.inbox.claim(target, position.turn)   // ① 取用户输入
​
  // ② 组装 Sections(系统提示词)
  const assembly = await this.loopCtx.systemPrompt.assemble(assembleContextFor(this, signal))
​
  // ③ 渲染动态上下文(快照)并注入
  const sections = renderContextSections(assembly)
  const context = this.runtimeContext.project(joinContextSections(sections), sections)
​
  // ④ waterfall:agent-instructions 在这里把 AGENTS.md 等折叠进消息
  const decision = await this.dispatch.waterfall(
    'agent/pre-step', { messages: claimed, ...position, signal },
    () => Promise.resolve({ kind: 'enter', messages: context === undefined ? claimed : [...claimed, context] }),
  )
  return decision.kind === 'reject' ? decision : { ...decision, assembly }
}

四步各干一件事:

  1. 取用户输入------claim 走 Inbox 里排队的消息;
  2. 组装 Sections ------assemble() 收集所有 Section、排序;之后在 step 里 renderPrompt() 渲染成 system 文本(静态部分这里只"收集排序",真正渲染在 step);
  3. 渲染并注入动态上下文 ------renderContextSections() 渲染快照 → runtimeContext.project() 比较并注入(变化才注入,见第四节);
  4. waterfall 折叠 Agent Instructions ------agent/pre-step 是 waterfall 事件,agent-instructions 监听它(322 行),把 AGENTS.md 等指令折叠进消息列表。

各部分拼到哪?system 字段 + messages 数组

拼接的本质:Sections 渲染成 system 字段的一段文本,其余内容按顺序塞进 messages 数组Message[],每段内容是一个元素):

位置规则:用户输入在最前(messages[0]),指令紧跟其后(折叠在 claimed 之后,messages[1]),动态快照随后(messages[2]),历史消息垫底(messages[3..N])------每一步都是一个数组元素,模型按顺序读。

拼完的请求长什么样

先看顺序:

ini 复制代码
┌─ system 字段(静态前缀,缓存命中)─────────────┐
│ 1. harness:identity (-100)                    │
│ 2. deployment:persona (0)                     │
│ 3. tool:read (100)                            │
│ 4. ...(其他工具引导,100-116)                  │
└──────────────────────────────────────────────┘
​
┌─ messages 数组(按顺序)──────────────────────┐
│ [0] 用户输入                                   │
│ [1] Agent Instructions:AGENTS.md / CLAUDE.md  │ ← 规则,pre-step 折叠进来
│ [2] 动态上下文快照                              │ ← 委托 / 沙箱 / 审批
│ [3..N] 历史消息                                │
└──────────────────────────────────────────────┘

再看数据结构------发给 LLM 的请求是 system / messages / tools 三个字段systemtools 是真实文本和真实 schema,messages 内容为示意;system 实际是全部注册 Section 拼的,这里示意三个):

swift 复制代码
{
  "provider": "deepseek",
  "model": "deepseek-chat",
  "system": "You are an AI agent powered by DeepSeek Harness.\n\n你是资深架构师,说话直接。\n\nUse the read tool --- not shell commands like cat --- to inspect text files. Results include line numbers. Use offset and limit to continue reading large files.",
  "messages": [
    { "role": "user", "content": "列出当前目录文件" },
    { "role": "user", "content": "(AGENTS.md 指令,agent-instructions 折叠进来)" },
    { "role": "user", "content": "(动态快照:委托状态 / 沙箱策略 / 审批状态)" },
    { "role": "assistant", "content": "我来查看。", "tool_calls": [{ "id": "call_1", "type": "function", "function": { "name": "bash", "arguments": "{"command":"ls"}" } }] },
    { "role": "tool", "tool_call_id": "call_1", "content": "src/ docs/ README.md" }
  ],
  "tools": [
    { "type": "function", "function": { "name": "read", "description": "Read a UTF-8 text file and return line-numbered content.", "parameters": { "type": "object", "properties": { "file_path": { "type": "string", "description": "Path to read, resolved by the filesystem backend." }, "offset": { "type": "number", "description": "1-based first line to return. Defaults to 1." }, "limit": { "type": "number", "description": "Maximum number of lines to return." } }, "required": ["file_path"] } } },
    { "type": "function", "function": { "name": "bash", "description": "Execute a bash command.", "parameters": { "type": "object", "properties": { "command": { "type": "string", "description": "The bash command to execute." }, "description": { "type": "string", "description": "Clear, concise description of what this command does." }, "timeoutMs": { "type": "number" }, "workdir": { "type": "string" } }, "required": ["command", "description"] } } }
  ]
}

三个字段的分工:system 是拼装好的 Sections 文本(静态前缀);messages 是数组------用户输入、指令、快照、历史按序排列,模型与工具的往返也都在这里(role: user / assistant / tooltool_callstool_call_id 配对);tools 是工具 schema 列表(结构化定义,模型据此生成合法的 tool_calls)。

顺序就是效率:静态(system)在前命中缓存、动态(messages)在后不碰前缀、工具 schema 结构化驱动合法 tool_calls------每一层的位置都有目的,AI 在正确的顺序里读到正确的内容。

总结

回到开头:模型每次请求看到的上下文,是三层拼出来的。看完你会明白,每一层都有自己的位置和理由:

  • Sections(System) ------稳定前缀,管"你是谁、能做什么",命中缓存;
  • Agent Instructions(User) ------规则和记忆,动态加载、自动更新,不污染前缀;
  • Contexts(User) ------运行时快照,变化才注入。

三层都在 preStep(每次调模型前)汇合:assemble Sections → 注入 Contexts → waterfall 折叠指令------然后一起进模型请求。

值得带走的是一个判断:什么该进 System、什么该进 User? 稳定的、要命中缓存的放 System(静态前缀);会变的、跟着现场走的放 User(动态尾部)。这不是 DeepSeek Harness 的细节,是任何 Agent 系统拼上下文时都要做的取舍------把变化的内容塞进稳定前缀,缓存就废了。

开头那句话,现在可以回收了:模型看到的上下文是三层拼出来的,不是哪一层单独写出来的------每一层由谁贡献、放哪、怎么更新,都写在代码里。

系列文章导航

编号 文章 对应模块
000 5 分钟看懂 DeepSeek Harness 全景图
001 动力引擎:Agent Loop 是怎么转起来的 AGENT LOOP
002 上下文拼接:系统提示词与动态内容(本页) SYSTEM PROMPT
003 LLM 适配层:从 DeepSeek 到任意模型 LLM
004 工具系统设计:三段 waterfall 管道 TOOLS
005 追加式事件日志:Session 设计 SESSION
006 上下文压缩:长对话管理 COMPACTION
007 能力接缝模式:定义/实现/使用分离 SHELL + 全部 Seam
008 Cordis 插件体系 CORDIS
009 Subagent:委托与隔离(规划中) SUBAGENT
010 Workflow:模型驱动的多 Agent 协作(规划中) WORKFLOW
011 Guard + Permission + Sandbox(规划中) GUARD / INTERACTION / SANDBOX
012 Plan + Preset + Context + Todo(规划中) PLAN / PRESET / CONTEXT / TODO
013 Hooks:Claude Code / Codex hook 桥(规划中) HOOKS

常见问题 FAQ

Q: Sections、Agent Instructions、Contexts 三层的区别是什么?

A: 按角色和内容分:Sections 是 System 角色(身份、性格、工具引导),静态、按 order 排序、命中缓存;Agent Instructions 是 User 角色(AGENTS.md / CLAUDE.md 等规则记忆),动态加载、工具改动自动重载;Contexts 是 User 角色(委托、沙箱、审批等运行时快照),变化才注入。System 管"你是谁、能做什么",User 管"项目规则、当前状态"。

Q: Agent Instructions 什么时候加载、什么时候更新?

A: 加载在 agent/pre-step 事件(agent-instructions 的 322 行监听),每次 Step 前从磁盘读 AGENTS.md / CLAUDE.md 等;更新在 tools/result 事件(350 行)------工具执行完检查它是否触及了指令文件,触及就标记,下一个 pre-step 重读。改规则文件不用重启。

Q: 为什么规则和记忆放 User 消息而不是 System?

A: 因为 System 是稳定前缀,要命中 LLM 缓存------规则文件会变(改 AGENTS.md 就要重读),塞进 System 会让前缀变化、缓存全部失效。放 User 消息尾部,变化不影响前缀。

Q: 拼接发生在哪个环节?

A: preStep(agent.ts 225-243),每次调模型前:① claim 用户输入 ② assemble Sections ③ renderContextSections + project 注入 Contexts ④ waterfall 让 agent-instructions 折叠指令。静态渲染成 system 在 step 里做(renderPrompt)。

Q: MEMORY.md 是默认加载的吗?

A: 不是。agent-instructions 的默认候选是 AGENTS.md / CLAUDE.md(基线)+ AGENTS.local.md / CLAUDE.local.md(本地)。MEMORY.md 需要部署时往 instructionFileCandidates 配置里加才会被加载。

Q: 工具是怎么拼进上下文的?工具太多怎么办?

A: 工具进上下文是两条路:工具引导 Section (order 100-116,散文)教模型"怎么用",拼进 system 文本;工具 schema (结构化定义)单独进请求的 tools 参数(agent.ts 490 行 request.tools),模型看到的是 API 级定义列表。

工具太多会不会挤爆上下文?DH 没有做运行时裁剪 ------没有工具数量上限、没有 token 预算裁剪、description 也不截断:如果 1 万个工具都在同一个 Agent 的作用域里可见,它就把 1 万份 schema 全拼进请求。它做的是结构性控制:schema 精简(每份只保留 name/description/parameters 三字段)、作用域隔离(子 Agent 只看到自己作用域的工具 + 全局可见的,单个 Agent 实际可见是几十个量级)、部署配置(preset 禁用限制工具)、缓存(词法排序稳定命中)。工具数量的最终控制靠部署设计,不靠框架自动裁剪。

Q: 工具列表顺序为什么固定?

A: 为了 LLM 缓存。prompt caching 按请求前缀匹配,工具引导(Section 100-116)是前缀的一部分------顺序稳定才能命中缓存。所以 order 是写死的数字、工具 schema 按词法排序,而不是谁先注册谁在前。

参考链接

相关推荐
jobBridge211 小时前
大模型到底是怎么"想"的?我把 Transformer 拆开,发现它其实是个"接词狂魔"
人工智能·后端·编程语言
水如烟1 小时前
孤能子视角:感质论——关系场的内摩擦显影:自指折返时的质地涌现
人工智能
AC赳赳老秦1 小时前
风控岗应用:OpenClaw 采集公开司法与经营异常数据,自动生成企业风险评估报告
大数据·c语言·数据库·人工智能·python·php·openclaw
析稿Ai写作工具1 小时前
无限画布+服装带货:一套完整的AI视频生成方案(含提示词工程)
人工智能
还不秃顶的计科生2 小时前
具身智能论文学习8:Octo: An Open-Source Generalist Robot Policy
人工智能·深度学习·学习·机器学习·语言模型·vla·vlm
新知图书2 小时前
6.4 关键时刻:它自己修好了 Bug
人工智能·agent·ai agent·智能体
贾维思基2 小时前
这次不是演习,Vibe Coding复刻联机桌游!
架构
深海鱼在掘金2 小时前
深入浅出RAG——第7章:基础篇实战:文档问答机器人
人工智能·typescript·命令行
Jay80592 小时前
一文讲清楚 Epoch、Batch 和 Iteration 的区别
人工智能