Context Engineering:让 Agent 在当前步骤看到正确的事实

AI Agent 工程化系列第三篇。本文以 Cherry Studio 的运行时为例,讲解对话历史、附件、知识库、记忆与 Agent Session 如何共同构成模型上下文,并给出关键算法的伪代码与工程取舍。

写在前面:不是上下文越长,Agent 就越聪明

做 Chat 时,最省事的做法是把历史消息带上;做 Agent Work 后,这招很快就不够用了。附件、知识库、工具结果、长期记忆、工作区状态都想进来,模型上下文像一个不断往里塞东西的背包。

背包大不代表好用:无关资料会稀释重点,长文本会吞掉 token,跨用户或跨工作区的数据更不能"顺手带上"。

因此,团队在研究 Cherry Studio 时,重点不是"它能塞多少上下文",而是"它如何让模型在这一步只看到该看的事实"。这正是 Context Engineering 要解决的问题。

1. Context Engineering 解决什么问题

模型输出的质量上限由当前可见上下文决定。

text 复制代码
模型输出 = f(System Prompt, 消息历史, 附件, 工具结果, 检索资料, 当前运行时状态)

因此 Context Engineering 的问题不是"如何塞进更多信息",而是:

  1. 哪些信息是当前任务真正需要的?
  2. 信息以什么形式进入模型:原文件、提取文本、摘要、检索结果还是工具句柄?
  3. 如何控制 token、延迟、隐私和噪声?
  4. 如何确保模型能回到原始证据,而不是根据片段猜测?
  5. 用户继续对话、切换分支、插话或审批后,上下文如何保持一致?

一个实用的目标函数是:

text 复制代码
最大化:任务所需证据的覆盖率与可信度
最小化:无关 token、过期事实、敏感数据暴露和上下文冲突

Prompt Engineering 定义"模型应怎样做";Context Engineering 决定"模型此刻根据什么做"。

2. 回到 Cherry Studio:先把上下文拆开

Cherry Studio 不将所有数据混成一个长字符串,而是由不同层分别负责:

Context 层 内容 主要来源 进入模型的方式
System 产品规则、Assistant 指令、工具策略 Prompt 构造器 system instructions
Conversation 当前对话分支中的用户、助手与工具消息 SQLite / 临时会话 messages
Attachments 文件、图片、PDF、音频、视频 FileManager / FileProcessing 原生文件或提取文本
Knowledge 私有知识库资料 KnowledgeService kb_* 按需工具结果
Memory 身份、偏好、长期事实、事件 Agent 数据目录 Prompt 回灌或 memory tool
Runtime 工作区、语言、会话状态、resume token Agent Session Runtime settings / SDK context

这意味着"上下文"不是单一模块,而是一组数据选择与表示算法。先分层,再决定每层何时进场,能避免所有信息在一开始就挤进同一个请求。

3. 关键算法一:别把整棵消息树都塞进去

3.1 为什么历史不是简单 append

持久化 Chat 支持 regenerate 和多分支回复。用户可以从某条历史消息继续,产生新分支;因此一个 topic 内的全部消息不能都发送给模型。

正确的历史应是:

text 复制代码
根节点 → 当前 anchor 的唯一路径

而不是:

text 复制代码
该 topic 的全部消息

Cherry Studio 的 PersistentChatContextProvider.buildHistory 按 anchor 回溯路径,再应用"清除上下文"边界。

伪代码:

ts 复制代码
function buildHistory(anchorMessageId) {
  // 返回从根到 anchor 的单一路径,排除兄弟分支
  path = messageService.getPathToNode(anchorMessageId)

  // clear-context 是显式上下文边界,而不是删除历史数据
  lastClearIndex = findLastIndex(path, message =>
    message.parts.contains("clear-context")
  )

  visiblePath = path.slice(lastClearIndex + 1)

  return visiblePath.map(message => ({
    id: message.id,
    role: toContentRole(message.role),
    parts: message.parts
  }))
}

该算法复杂度与当前分支路径长度成正比:

text 复制代码
时间复杂度:O(depth)
空间复杂度:O(depth)

其中 depth 是根节点到 anchor 的消息数量,通常远小于 topic 中全部消息数量。

3.2 clear-context 为什么是逻辑边界

清除上下文并不删除数据库记录。原消息仍可展示、搜索、审计和作为分支历史存在;只是后续模型调用不再看到它之前的内容。

text 复制代码
M1 → A1 → M2[clear-context] → A2 → M3

模型看到:A2 → M3
数据库仍保存:M1 → A1 → M2 → A2 → M3

这个设计避免了两类问题:

  • 用户希望"重新开始讨论",却丢失可见聊天记录。
  • 通过物理删除实现压缩,导致回溯、审计与分支关系损坏。

3.3 当前边界:显式清除,不是自动压缩

普通持久化 Chat 目前以明确的 clear-context part 决定历史边界,并不会依据模型上下文窗口自动摘要或裁剪整段历史。

这是一个刻意需要产品决策的边界。自动压缩虽然能避免超窗,却可能丢失工具状态、审批结果、用户约束和引用来源。若未来引入自动压缩,应把它建模为一条可持久化、可检查、可重放的摘要消息,而不是请求前临时修改历史。

4. 关键算法二:根据模型能力路由附件

同一附件不应以固定方式进入每个模型。例如,视觉模型应接收原图;文本模型则需要 OCR 文本。Cherry Studio 的原则是:

每个附件根据 Provider 与模型能力,选择原生输入或提取文本;模型无需调用工具也能看到附件基本内容。

4.1 附件路由决策

ts 复制代码
function prepareAttachment(part, modelCapabilities, requestContext) {
  if (!part.hasFirstPartyFileEntry) {
    // 外部/gateway 文件保持兼容的原始处理路径
    return materializeOrReadableFailureNote(part)
  }

  file = fileManager.getById(part.fileEntryId)
  type = classify(file.extension)

  if (isNativeSupported(type, modelCapabilities)) {
    native = materializeNativeFilePart(part)
    return native ?? unreadableFileNote(part.filename)
  }

  text = extractByType(type, file)
  return inlineWithCap(part.modelFacingHandle, text, requestContext)
}

isNativeSupported 的概念逻辑:

ts 复制代码
function isNativeSupported(fileType, capabilities) {
  switch (fileType) {
    case "image": return capabilities.vision
    case "audio": return capabilities.audio
    case "video": return capabilities.video
    case "pdf": return capabilities.nativePdf
    default: return false
  }
}

非原生文件的降级路径:

文件类型 非原生模型看到的内容
图片 OCR 文本
PDF / Office / 文本 / 代码 提取文本
音频 / 视频 明确的不支持提示
二进制文件 明确的不支持提示
读取或解析失败 could not read this file 提示

这里的关键不是"尽量解析一切",而是永远不要静默丢掉附件。模型必须知道某个附件存在但当前不可读,否则它会基于缺失信息给出过度自信的结论。

4.2 为什么原生文件优先

将原生图片或 PDF 总是先 OCR 抽取为文本,会损失布局、表格、图形和多模态信息;同时会让模型能力向低能力 Provider 退化。

反过来,完全只提供 read_file 工具也不正确:非工具模型或未主动调用工具的模型会完全看不到附件内容。

正确策略是:

text 复制代码
模型原生支持 → 发送原文件
模型不原生支持 → 主动提供可读文本
文本过长 → 内联前段 + 提供受限分页工具
无法读取 → 明确说明失败

5. 关键算法三:截断、分页与安全文件句柄

附件全文可能很大。如果不加控制,单个 PDF 就足以挤掉整个对话历史;如果简单截断,又会让模型无法获得后半部分内容。

Cherry Studio 使用"内联首段 + 按需分页"的混合策略。

ts 复制代码
function capInlineText(handle, text, isToolCapable, cap) {
  if (text.length <= cap) {
    return text
  }

  end = surrogateSafeEnd(text, cap)
  head = text.slice(0, end)

  if (!isToolCapable) {
    return head + `[truncated ${end}/${text.length} chars]`
  }

  return head +
    `[truncated ${end}/${text.length} chars; ` +
    `call read_file("${handle}", offset=${end}) for more]`
}

5.1 为什么需要 surrogateSafeEnd

JavaScript 字符串以 UTF-16 code unit 计数。若直接在 cap 位置 slice,可能切断一个代理对,例如 emoji 或部分非 BMP 字符,产生非法或显示异常文本。

概念算法:

ts 复制代码
function surrogateSafeEnd(text, requestedEnd) {
  end = min(requestedEnd, text.length)

  if (isHighSurrogate(text[end - 1]) && isLowSurrogate(text[end])) {
    return end - 1
  }

  return end
}

这是一类很小但重要的 Context Engineering 细节:错误截断不会只影响显示,还可能改变模型对结构化数据、路径或代码片段的理解。

5.2 模型可见的不是内部文件 ID

read_file 使用模型可见文件名作为 handle,而不是暴露 fileEntryId。同名附件需要生成稳定且唯一的别名:

ts 复制代码
function uniqueHandle(displayName, used) {
  base = trim(displayName) || "file"
  candidate = base
  suffix = 2

  while (used.has(candidate)) {
    candidate = `${base} (${suffix})`
    suffix += 1
  }

  used.add(candidate)
  return candidate
}

随后只允许模型在本轮附件 allow-list 内解析 handle:

ts 复制代码
function readFileForModel(handle, offset, attachmentAllowList) {
  ref = attachmentAllowList.find(item => item.handle === handle)
  if (!ref) {
    throw sanitizedError("Attached file not found")
  }

  return pageExtractedText(ref.fileEntryId, offset)
}

这个设计同时实现:

  • 防止模型猜测或枚举内部文件 ID。
  • 防止本轮模型读取未附加的用户文件。
  • 让错误信息保持在用户可理解的文件名层,而不泄露内部路径和存储结构。

6. 关键算法四:知识库 Scope 的安全收窄

知识库 Context 不应通过"把所有库都搜索一遍"实现。Agent 需要先有可验证的可见范围。

Cherry Studio 将静态绑定视为 ceiling,而不是每轮选择的默认值:

ts 复制代码
function resolveKnowledgeBaseScope(configuredIds, selectedIds) {
  if (configuredIds is empty) {
    return canonicalize(selectedIds)
  }

  if (selectedIds is empty) {
    return canonicalize(configuredIds)
  }

  configured = new Set(configuredIds)
  narrowed = selectedIds.filter(id => configured.has(id))

  if (narrowed is empty) {
    // 全部选择越界时,保留原静态绑定,避免无意禁用 Agent 知识
    return canonicalize(configuredIds)
  }

  return canonicalize(narrowed)
}

function canonicalize(ids) {
  return sort(unique(ids))
}

这段逻辑有三个语义:

  1. 静态绑定是权限上限:Renderer 传来的 ID 不能扩大 Agent 可见库。
  2. 本轮选择是收窄器:用户可以临时只让 Agent 使用某几个已绑定库。
  3. 输出规范化:去重与排序使 scope 可比较、可缓存并可作为运行时重建签名。

需要注意不同运行时的空 scope 语义。共享检索核心中空 allow-list 可以表示"不额外限制";但 Agent Session 会在没有有效 scope 时隐藏或拒绝知识库工具,采用 fail-closed 策略。产品层必须明确"默认允许搜索全部个人库"还是"必须显式选择知识库",不能把这一差异留给调用方猜测。

7. 关键算法五:Agentic RAG 的渐进式取证

RAG 不是把检索结果自动附在每次请求后。对于知识库问答,Cherry Studio 提供面向模型的逐步工具:

text 复制代码
kb_list → kb_search → kb_read

可将其理解为逐层缩小搜索空间:

ts 复制代码
async function answerWithKnowledge(question, allowedBaseIds) {
  bases = await kb_list({ allowedBaseIds })
  candidates = chooseRelevantBases(question, bases)

  hits = await kb_search({
    baseIds: candidates,
    query: question
  })

  evidence = await kb_read({
    baseId: hits[0].baseId,
    conceptId: hits[0].conceptId
  })

  return generateAnswer(question, evidence, citationsFrom(hits))
}

这不是要求模型机械地执行固定链路,而是表达三种不同粒度的信息访问:

工具 信息粒度 适用问题
kb_list 知识库与资料目录 "哪些资料可能有关?"
kb_search 相关 chunk "哪些片段支持当前问题?"
kb_read 文档全文或指定页/grep "片段是否被断章取义?需要精确引用什么?"

它比"检索 topK 后直接回答"更可靠,因为模型可以在证据不足时主动扩大或深入查询;同时比"整库加入上下文"更节省 token。

8. Agent Session 与普通 Chat 的 Context 差异

普通 Chat 的 Context 核心是消息树路径;Agent Session 的 Context 还包含工作区、runtime connection 和可恢复会话状态。

维度 普通 Chat Agent Session
主上下文 当前 message-tree 分支 当前 turn + driver 会话状态
工作目录 不作为通用 Chat 语义 session workspace 是 cwd
用户插话 运行中的 turn yield 后启动 continuation 可在下一次工具调用前注入 steer,或加入 pending queue
恢复锚点 持久化消息历史 最新 assistant 的 opaque resume token
Prompt Assistant prompt + 当前工具策略 PromptBuilder + workspace + runtime + session policy

Agent Session 中收到 live follow-up 时,不应该简单把新文本塞进正在进行的模型输入。正确流程取决于 Driver 是否支持 redirect:

ts 复制代码
function handleLiveFollowUp(session, message) {
  if (session.hasLiveTurn && session.driver.supportsRedirect) {
    session.driver.redirect({
      message,
      systemReminder: true
    })
    return
  }

  session.pendingTurns.push(message)
  scheduleNextTurnAfterCurrentCompletes(session)
}

对于 Claude Code Driver,redirect 会暂存 steer,并在下一次 PreToolUse 时作为 additionalContext 注入。这样上下文变更发生在工具调用边界,而不是任意流式 token 中间。

9. Context 预算:应该控制什么

Context 预算不应只看 token 总数。至少需要分别观察:

text 复制代码
预算 = 历史消息 + Prompt + 附件文本 + 工具定义 + 工具结果 + RAG 证据 + 模型预留输出

其中风险最大的通常不是用户一句话,而是:

  • 多轮历史中重复出现的附件文本。
  • 长文件提取内容。
  • 大型 MCP 的工具定义。
  • 未裁剪的工具返回。
  • 大量检索 chunk 或完整网页正文。

建议为每类 Context 设置不同策略:

类型 优先策略
系统策略 静态化、缓存、版本化
历史消息 分支选择、显式 clear、未来可持久化压缩
附件 原生优先、提取文本 cap、按需分页
知识库 list/search/read 渐进检索、引用保留
工具结果 结构化、限长、保留可继续读取的句柄
记忆 长期事实内联,事件日志按需检索

任何压缩算法都应保留以下信息:

  • 用户的硬约束和未完成任务。
  • 工具执行状态与审批状态。
  • 关键实体、文件名、路径、引用和错误。
  • 能回到原始材料的定位符。

10. 这些 Context 坑,最好提前绕开

反模式:将整个会话、所有附件和所有资料直接发送

后果是 token 爆炸、注意力稀释和敏感信息暴露。应改为分支选择、cap、分页和渐进检索。

反模式:把附件可见性完全交给工具调用

弱模型或未调用工具的模型会看不到附件。应至少内联受控的可读内容或不可读提示。

反模式:仅在 UI 层限制知识库选择

Renderer 输入不可信。必须在主进程根据静态绑定重新计算 scope。

反模式:截断后不告诉模型还有内容

模型会把前半段误认为全文。对于工具模型,应提供包含 offset 的 read_file 提示。

反模式:让自动摘要悄悄改写会话事实

摘要一旦取代原历史却不可追溯,用户无法验证信息是否丢失。应持久化摘要、记录其覆盖范围,并保留回读原消息的能力。

11. 小结

Context Engineering 的成熟标志不是支持更多输入类型,而是能够稳定回答:

  1. 模型这一轮实际看到了什么?
  2. 为什么是这些内容,而不是更多或更少?
  3. 附件、资料和历史能否回到原始证据?
  4. 当上下文超出预算、用户插话或文件不可读时,系统如何降级?
  5. 这些规则是否在主进程和运行时边界被强制执行?

一句话总结:

好的 Context Engineering 不追求让模型知道一切,而是让模型在每个决策点都拿到足以完成任务、且可验证与可控的信息。

继续阅读

相关推荐
百度Geek说1 小时前
面向 Coding Agent 的多仓库 Git Worktree
人工智能
张小泡泡1 小时前
AUTO_EVAL:面向大语言模型的多层次自动化评测框架
论文阅读·人工智能·语言模型·自然语言处理·自动化·微调
AI工具测评与分析1 小时前
Seedance2.5 赋能飙算画影AI无限画布,无边画布进阶专业创作工作台
人工智能·信息可视化·ai作画·视频生成·ai视频·爆款视频
嘟嘟07171 小时前
从手写 HashRouter 到 React Router:逐层拆解 SPA 前端路由的完整实现
前端·javascript
前端开发江鸟1 小时前
我没有自研 AI 中转站:8 小时跑通 New API、DeepSeek 与 Codex Coding Plan
人工智能
zlycheng1 小时前
AI+CNC深度融合,全面革新机加工运营模式,激活制造新动能
人工智能·制造
哦哦~9212 小时前
AI赋能复合材料力学:从数据驱动到物理信息神经网络与多尺度仿真
人工智能·深度学习·神经网络·复合材料力学
腾渊信息科技公司2 小时前
工业机器视觉深度学习落地:标注、训练与产线部署全流程避坑思路
人工智能·深度学习
天国梦2 小时前
2026年英语教学数字化工具深度测评:天学网、腾讯英语君、翼课网横向对比
人工智能·学习