Hello-Agents 第二部分-第九章总结:上下文工程
作者:逆境不可逃
技术永无止境
希望我的内容可以帮助到你!!!!!
大家吼 ! 我是 逆境不可逃 今天给大家带来文章《Hello-Agents 第二部分-第九章总结:上下文工程》.
Hello-Agents 官方地址:datawhalechina/hello-agents: 📚 《从零开始构建智能体》——从零开始的智能体原理与实践教程
上篇文章《Hello-Agents 第二部分-第八章总结:记忆与检索》
https://blog.csdn.net/2401_87662859/article/details/161246945?spm=1001.2014.3001.5501
前言
本章的核心目标是把第 8 章的记忆与 RAG 能力继续往工程化方向推进:不再只问 “Agent 能记住什么、能检索什么”,而是进一步问 “每一次调用模型前,应该把哪些信息、以什么结构、在多大预算内交给模型”。这就是上下文工程Context Engineering。
一句话概括:提示工程关注怎么写指令;上下文工程关注怎样持续管理模型可见的全部 token,包括系统提示、工具说明、外部检索结果、对话历史、笔记、文件内容和任务状态。它的目标不是把所有信息都塞进窗口,而是在有限注意力预算内构造高信号、低噪声、可复用、可评估的上下文。
配套代码建议按下面顺序阅读:
| 文件 | 关注点 |
|---|---|
01_context_builder_basic.py | ContextBuilder基础用法、ContextConfig参数、结构化上下文构建 |
02_context_builder_with_agent.py | 将ContextBuilder集成到SimpleAgent,让 Agent 每轮自动构造上下文 |
03_note_tool_operations.py | NoteTool的create/read/update/search/list/summary/delete基础操作 |
04_note_tool_integration.py | NoteTool与ContextBuilder协作,把历史笔记注入上下文 |
05_terminal_tool_examples.py | TerminalTool的文件探索、数据分析、日志分析、代码库分析和安全限制 |
06_three_day_workflow.py | 三天代码库维护流程:探索、分析、规划、复盘 |
codebase_maintainer.py | 长程代码库维护助手核心实现,整合ContextBuilder + NoteTool + TerminalTool + MemoryTool |
codebase/ | 用于演示代码分析的样例代码库 |
data/sales_2024.csv | 用于演示即时数据文件分析的样例 CSV |
1. 上下文工程是什么
LLM 的一次调用本质上只看当前传入的 token。这里的 token 不只是用户问题,还包括系统提示、开发者指令、工具描述、历史消息、检索结果、记忆、文件片段和中间推理产物。上下文工程就是管理这组 token 的工程方法。
它和提示工程的区别可以这样理解:
| 对比项 | 提示工程 | 上下文工程 |
|---|---|---|
| 核心问题 | 怎样写好 prompt | 怎样组织模型可见的全部信息 |
| 主要对象 | 系统提示、任务描述、示例 | 提示、工具、历史、记忆、RAG、文件、笔记、状态 |
| 时间尺度 | 多数是单轮或短任务优化 | 面向多轮、长时程、跨会话任务 |
| 工程目标 | 让模型更容易按指令输出 | 让模型持续获得正确、相关、适量的信息 |
| 典型风险 | 指令含糊、格式不稳 | 上下文污染、信息过载、长程状态丢失 |
本章强调的关键判断是:上下文是有限资源,并且具有边际收益递减。即使模型支持很长的上下文窗口,也不意味着越长越好。长上下文会引入更多干扰、更多相互冲突的信息和更高的推理成本,模型从长窗口中稳定找回关键信息的能力也会下降。
2. 为什么上下文工程重要
2.1 上下文腐蚀
上下文腐蚀context rot指的是:随着上下文窗口中 token 数量增加,模型从上下文中准确回忆、定位和使用关键信息的能力反而下降。它不是突然失效,而是性能逐渐退化。
原因主要有三类:
- 注意力被拉薄:Transformer 中 token 之间存在大规模相互作用,窗口越长,模型越难稳定关注真正关键的依赖。
- 噪声增多:无关历史、重复工具输出、过期计划和冗长日志都会稀释高价值信息。
- 训练分布限制:模型虽然能处理长上下文,但训练中短序列和局部依赖更常见,超长、复杂、全局依赖的稳定性天然更难。
所以,构建 Agent 时不能把 “长上下文窗口” 当成免管理的仓库。真正可靠的做法是把上下文视为注意力预算,持续选择、压缩、结构化和外部化信息。
2.2 有效上下文的组成
有效上下文的目标是:用尽可能少但足够的 token,让模型最大概率产生期望行为。它通常包括三类核心元素。
系统提示
系统提示负责定义角色、边界、工作原则、工具使用规则和输出格式。常见的两个极端都不好:
- 过度硬编码:把大量脆弱的 if-else 写进提示,维护困难,且容易和真实情况冲突。
- 过于空泛:只说 “你是专业助手”“请准确回答”,缺少可执行的判断标准和输出要求。
更好的写法是把系统提示分区,例如[Role & Policies]、[Task]、[Tools]、[Output],用清晰层次表达 “必须遵守的规则” 和 “可灵活判断的策略”。
工具
工具决定 Agent 能访问哪些信息、能采取哪些行动。工具设计不只是功能问题,也是上下文问题:工具描述、入参、返回值都会消耗 token 并影响模型决策。
好的工具应该职责单一、边界清楚、参数明确、错误可恢复、输出 token 友好。工具集也应避免臃肿。如果多个工具能力重叠,模型会把大量注意力浪费在 “该用哪个工具” 上。一个小而清晰的最小可行工具集,通常比一个大而模糊的工具箱更稳定。
示例
Few-shot 示例的价值在于直接展示期望行为。示例不是越多越好,而是要多样、典型、能覆盖关键边界。对模型而言,一个高质量示例往往比一大段抽象规则更可执行。
2.3 JIT 上下文与渐进式披露
传统 RAG 常见做法是 “推理前一次性检索”:先根据 query 找一批相关片段,再统一放入上下文。第 9 章进一步强调Just-in-time上下文:不要预先加载全部资料,而是保留文件路径、查询语句、URL、笔记 ID 等轻量引用,让 Agent 在运行时通过工具按需获取。
例如代码审查中,没必要一开始把 50 个文件全部塞入上下文。更好的方式是:
- 先读取目录结构和 README。
- 根据任务定位关键模块。
- 用
grep/find/head/tail查找具体函数、TODO、错误日志或配置项。 - 只把当前判断需要的片段放进上下文。
- 把阶段性结论写入笔记,后续按需拉回。
这就是渐进式披露progressive disclosure:每一步探索都会产生新的线索,新线索又指导下一步探索。目录名、文件大小、时间戳、错误栈、函数名都不是最终答案,却能帮助 Agent 决定下一步看哪里。
JIT 的优势是实时、低成本、不容易受过时索引影响;代价是更慢,需要工具设计和探索策略足够好。实践中通常采用混合策略:预置少量高价值上下文,例如 README、项目约定、当前任务说明,再允许 Agent 用工具动态检索细节。
3. 长时程任务的三类策略
长时程任务的难点不是单轮回答,而是跨很多轮、很多工具调用甚至很多天后仍然保持目标一致、状态清楚、行动连贯。本章给出三类主要策略。
| 策略 | 解决的问题 | 典型做法 | 适用场景 |
|---|---|---|---|
压缩整合Compaction | 历史太长,窗口接近上限 | 把历史高保真总结后重启上下文 | 长对话、长调试、连续研究 |
结构化笔记Structured note-taking | 关键状态需要跨窗口保存 | 把结论、阻塞、计划写入外部笔记 | 项目维护、研究任务、复杂计划 |
子代理架构Sub-agent | 单一上下文难以容纳并行探索 | 主代理规划汇总,子代理独立探索后回传摘要 | 大规模代码库分析、复杂研究、多方向排查 |
压缩整合要优先保证召回,也就是不要漏掉关键决策、未解决问题、接口约束和实现细节;其次再优化精确度,删除重复工具输出和噪声。
结构化笔记是本章落地的重点。它把 “当前不该塞进上下文,但未来可能重要” 的信息放到上下文外,必要时再检索回来。
子代理架构的关键价值是隔离噪声。主代理保持干净的高层上下文,子代理在自己的窗口中做深挖,最后只返回压缩后的结论。
4. ContextBuilder:上下文构建器
ContextBuilder是本章最核心的组件。它把上下文构建抽象成一条固定流水线:
Gather -> Select -> Structure -> Compress 汇集 选择 结构化 压缩这条流水线简称GSSC。它的设计目标是减少 Agent 中重复的上下文拼接代码,并让上下文构建过程可调试、可评估、可替换。
4.1 核心数据结构
ContextPacket是候选信息包。任何可能进入上下文的信息都先被包装成统一结构。
@dataclass class ContextPacket: content: str timestamp: datetime token_count: int relevance_score: float = 0.5 metadata: Optional[Dict[str, Any]] = None关键字段含义:
| 字段 | 作用 |
|---|---|
content | 实际文本内容 |
timestamp | 信息产生或更新的时间,用于新近性评分 |
token_count | 估算 token 数,用于预算控制 |
relevance_score | 与当前任务的相关性,范围通常是 0 到 1 |
metadata | 类型、来源、优先级、笔记 ID 等扩展信息 |
ContextConfig管理上下文构建参数。
config = ContextConfig( max_tokens=3000, reserve_ratio=0.2, min_relevance=0.1, enable_compression=True, recency_weight=0.3, relevance_weight=0.7, )其中reserve_ratio很重要,它为系统指令等高优先级信息预留空间,避免被历史消息或检索结果挤掉。relevance_weight与recency_weight控制 “相关性” 和 “新近性” 的权衡。
4.2 Gather:多源汇集
Gather负责收集候选信息。来源包括:
- 系统指令:最高优先级,通常必须保留。
MemoryTool:检索相关记忆。RAGTool:检索外部知识证据。- 对话历史:通常只取最近若干条,防止历史淹没当前任务。
- 自定义信息包:例如
NoteTool笔记、TerminalTool输出、人工传入的状态。
这一阶段的重点是容错。每个外部数据源都应该单独捕获异常,记忆检索失败不能导致整个上下文构建失败。
4.3 Select:智能选择
Select是 GSSC 的核心。它在 token 预算内选择最值得进入上下文的信息。
选择逻辑大致是:
- 先分离系统指令和普通信息,系统指令优先保留。
- 对普通信息计算相关性分数和新近性分数。
- 用加权公式得到综合分数:
combined_score = relevance_weight * relevance_score + recency_weight * recency_score- 过滤掉低于
min_relevance的信息。 - 按综合分数从高到低贪心填充,直到达到 token 上限。
这里的算法不复杂,但工程上很实用。它把 “保留什么” 变成可解释的排序问题。生产环境中,相关性可以从简单关键词重叠升级为向量相似度、BM25 或混合检索。
4.4 Structure:结构化输出
Structure把选中的信息组织成稳定模板。本章推荐的骨架类似:
[Role & Policies] 角色、规则、边界 [Task] 当前用户问题或任务 [Evidence] RAG 或外部知识证据 [Context] 历史消息、记忆、笔记、工具结果 [Output] 输出格式和要求稳定结构有三个好处:
- 模型更容易区分 “规则、任务、证据、上下文”。
- 人类更容易调试,能快速判断哪个分区引入了错误信息。
- 方便做 A/B 测试和日志评估。
4.5 Compress:兜底压缩
Compress在上下文超限时触发。代码中的基础实现是分区压缩:尽量完整保留前面的结构化分区,超限部分再截断并标记 “内容已压缩”。
更成熟的压缩策略可以分层:
| 情况 | 推荐策略 |
|---|---|
| 日志、工具输出、重复文本 | 截断、抽样、保留首尾 |
| 对话历史 | 摘要压缩,保留决策和未解决问题 |
| 代码文件 | 保留签名、关键函数、错误附近片段 |
| 任务计划 | 结构化摘要,保留状态、依赖、下一步 |
| 法规、合同、精确证据 | 尽量不摘要,改为引用片段和来源定位 |
压缩不是简单变短,而是保留任务继续推进所需的状态。最危险的压缩是把 “为什么这么做”“还没解决什么”“哪些约束不能破坏” 压掉。
4.6 代码示例对应关系
01_context_builder_basic.py演示了最小用法:
config = ContextConfig( max_tokens=3000, reserve_ratio=0.2, min_relevance=0, enable_compression=True, ) builder = ContextBuilder(config=config) context_str = builder.build( user_query="如何优化Pandas的内存占用?", conversation_history=conversation_history, system_instructions="你是一位资深的Python数据工程顾问。" )这里的核心不是 LLM 调用,而是build()返回一个结构化上下文字符串,可直接作为 system message 传给模型。
02_context_builder_with_agent.py把它封装进ContextAwareAgent.run():
optimized_context = self.context_builder.build( user_query=user_input, conversation_history=self.conversation_history, system_instructions=self.system_prompt, ) messages = [ {"role": "system", "content": optimized_context}, {"role": "user", "content": user_input}, ] response = self.llm.invoke(messages)这说明上下文工程最好不要散落在业务代码里,而应该变成 Agent 每轮运行前的标准步骤。
5. NoteTool:结构化笔记
NoteTool是面向长时程任务的结构化外部记忆。它和第 8 章的MemoryTool不冲突,二者侧重点不同:
| 工具 | 更适合保存 | 特点 |
|---|---|---|
MemoryTool | 对话记忆、用户偏好、经验、语义知识 | 更像智能体内部记忆,可结合向量检索 |
NoteTool | 项目状态、任务计划、阻塞点、阶段结论 | Markdown + YAML,人类可读,版本控制友好 |
5.1 为什么需要 NoteTool
长程任务中,很多信息不是一句普通记忆,而是需要持续维护的项目状态,例如:
- 当前重构做到哪一步。
- 哪些 blocker 还没解决。
- 哪些方案已经被否定,原因是什么。
- 下一步行动计划是什么。
- 某个结论来自哪个文件或哪次分析。
这些信息如果只放在对话历史里,很快会被上下文窗口挤掉;如果每次都重新分析,又浪费时间且容易前后不一致。结构化笔记提供了一个轻量、可编辑、可检索的外部状态层。
5.2 存储格式
NoteTool 采用 Markdown 正文加 YAML 前置元数据的混合格式。典型笔记可以设计为:
--- id: note_20250119_153000_0 title: 依赖冲突问题 type: blocker tags: - dependency - urgent created_at: 2025-01-19T15:30:00 updated_at: 2025-01-19T15:30:00 --- ## 问题描述 某些第三方库版本不兼容。 ## 影响范围 业务逻辑层的 3 个模块。 ## 下一步 1. 使用虚拟环境隔离。 2. 锁定版本。 3. 分析依赖树。这种设计的好处是:
- 机器可解析:YAML 元数据便于按类型、标签、时间检索。
- 人类可读:Markdown 正文能直接编辑和审阅。
- Git 友好:纯文本天然适合版本控制。
- 容易回注上下文:检索后可转换为
ContextPacket。
5.3 核心操作
03_note_tool_operations.py展示了 NoteTool 的完整生命周期:
| 操作 | 作用 | 关键参数 |
|---|---|---|
create | 创建笔记 | title,content,note_type,tags |
read | 读取指定笔记 | note_id |
update | 更新笔记内容或元数据 | note_id,content |
search | 按关键词检索笔记 | query,limit |
list | 按类型列出笔记 | note_type,limit |
summary | 生成笔记统计摘要 | 无或过滤参数 |
delete | 删除笔记 | note_id |
建议使用固定笔记类型:
| 类型 | 用途 | 上下文优先级 |
|---|---|---|
blocker | 阻塞问题、风险、待解决缺陷 | 最高 |
action | 下一步行动计划 | 高 |
task_state | 阶段进度、当前状态 | 高 |
conclusion | 已确认结论、设计决策 | 中高 |
reference | 参考资料、链接、文件位置 | 中 |
scratch | 临时探索记录 | 低,需定期整理 |
5.4 与 ContextBuilder 集成
04_note_tool_integration.py的关键思路是:每轮对话前先用 NoteTool 检索相关笔记,再把笔记转换成ContextPacket注入ContextBuilder。
简化流程如下:
用户输入 -> 检索 blocker 和相关笔记 -> notes_to_packets() -> ContextBuilder.build(additional_packets=note_packets) -> LLM 生成回答 -> 必要时把本轮结论保存为新笔记代码中的ProjectAssistant._retrieve_relevant_notes()采用了一个务实策略:
- 优先列出
blocker类型笔记。 - 再按当前 query 搜索相关笔记。
- 合并去重。
- 限制数量,避免笔记反过来挤爆上下文。
_notes_to_packets()则负责把笔记变成上下文候选信息:
ContextPacket( content=f"[笔记:{title}]\n{body}", timestamp=parsed_ts, token_count=len(content) // 4, relevance_score=0.75, metadata={ "type": "note", "note_type": note_type, "note_id": note_id, }, )这一步非常关键:NoteTool 本身只是存储工具,只有转换成ContextPacket后,它才进入 GSSC 选择与压缩流程。
5.5 使用建议
NoteTool 的难点不是能不能写笔记,而是笔记会不会变成新的噪声源。实践建议:
- 每条笔记只表达一个主题,避免 “大杂烩笔记”。
- 标题要可检索,例如 “OrderService 嵌套过深” 比 “发现一个问题” 更好。
blocker要少而准,避免所有问题都标成最高优先级。- 已解决的 blocker 应更新状态或转为 conclusion。
- 临时笔记要定期整理,重要内容上升为 task_state 或 conclusion,重复内容归档。
- 人类应能审阅和修改笔记,长程任务不能完全依赖模型自己维护状态。
6. TerminalTool:即时文件系统访问
TerminalTool是本章实现 JIT 上下文的关键工具。它允许 Agent 用受控命令即时探索文件系统,而不是预先把整个代码库、日志目录或数据文件全部索引进 RAG。
6.1 适用场景
05_terminal_tool_examples.py展示了四类典型场景:
| 场景 | 示例命令 | 价值 |
|---|---|---|
| 探索式导航 | ls,find,head | 快速了解目录和关键文件 |
| 数据文件分析 | head,wc,cut,sort,uniq | 低成本预览 CSV 结构和分布 |
| 日志分析 | tail,grep,awk | 定位最近错误和错误类型 |
| 代码库分析 | find,grep,wc | 查找 TODO、函数定义、代码规模 |
相比 RAG,TerminalTool 更适合实时、精确、低预处理成本的任务。比如查看 “最新 50 行日志” 或 “当前代码库里所有 TODO”,向量索引反而不是最直接的工具。
6.2 安全机制
让 Agent 执行命令很强大,也很危险。本章的 TerminalTool 设计了多层防护:
| 安全层 | 作用 | 示例 |
|---|---|---|
| 命令白名单 | 只允许相对安全的只读命令 | 允许cat/head/grep/find/wc,拒绝rm |
| 工作目录沙箱 | 只能访问指定 workspace 内部 | 拒绝cat /etc/passwd或cd ../../../etc |
| 超时控制 | 防止命令卡死或长时间扫描 | timeout=30 |
| 输出大小限制 | 防止一次返回巨量内容 | 超过上限截断 |
这说明 TerminalTool 不是通用 shell,而是 “安全受限的信息获取工具”。如果要扩展到写文件、执行测试、运行 git 等高风险操作,应引入更严格的审批和权限分级。
6.3 与其他工具协同
TerminalTool 的输出通常不应长期留在对话历史中,而应按价值分流:
TerminalTool 即时发现 -> 当前轮需要的片段:放入 ContextBuilder -> 重要结论:写入 NoteTool -> 用户偏好或稳定知识:写入 MemoryTool -> 大规模知识库:必要时进入 RAG 索引例如:
grep -rn "TODO"的完整输出可能很长,不适合长期保留。- 但 “支付模块有 3 个高优先级 TODO,集中在 order_service.py” 是结论,适合写入
conclusion或blocker。 - “下一步先补测试再重构” 是计划,适合写入
action或task_state。
7. 长程智能体实战:代码库维护助手
第 9 章最终把三个工具整合成 “代码库维护助手”。这个案例的价值在于,它不是单轮问答,而是跨天持续工作的 Agent。
7.1 场景
假设要维护一个中型 Python Web 应用,代码库包含几十个文件,存在技术债务,需要完成:
- 探索代码结构。
- 识别重复代码、复杂函数、缺少测试、TODO/FIXME。
- 记录问题和风险。
- 规划重构任务。
- 跨会话复盘进度。
这个任务天然超过单轮上下文窗口,也不适合一次性把所有文件塞给模型。
7.2 工具分层
代码库维护助手采用分层上下文管理:
| 层 | 工具 | 保存什么 | 生命周期 |
|---|---|---|---|
| 即时访问层 | TerminalTool | 文件片段、目录结构、日志、命令结果 | 当前轮或短期 |
| 会话记忆层 | MemoryTool | 近期对话、用户意图、关键交互 | 会话内到跨会话 |
| 持久笔记层 | NoteTool | blocker、action、task_state、conclusion | 跨天、跨会话 |
| 上下文编排层 | ContextBuilder | 从以上来源筛选后的高价值上下文 | 每次模型调用前 |
这四层合起来解决了长程任务的核心矛盾:信息很多,但模型每次只应看到当前最有用的一部分。
7.3codebase_maintainer.py的实现重点
当前代码中的CodebaseMaintainer是更 Agentic 的版本,使用FunctionCallAgent和ToolRegistry,让 Agent 自主决定是否调用工具,而不是写死每一步流程。
初始化阶段:
self.memory_tool = MemoryTool( user_id=project_name, memory_types=["working"], ) self.note_tool = NoteTool(workspace=f"./{project_name}_notes") self.terminal_tool = TerminalTool(workspace=codebase_path, timeout=60) self.context_builder = ContextBuilder( memory_tool=self.memory_tool, rag_tool=None, config=ContextConfig( max_tokens=4000, reserve_ratio=0.15, min_relevance=0.2, enable_compression=True, ), )run()的主流程是:
1. 根据用户输入检索相关笔记,尤其优先 blocker。 2. 将笔记转换为 ContextPacket。 3. 用 ContextBuilder 构建优化上下文。 4. 更新 FunctionCallAgent 的 system_prompt。 5. 让 Agent 自主调用 TerminalTool、NoteTool、MemoryTool。 6. 统计工具使用并更新对话历史。代码里提供了三个便捷模式:
| 方法 | 模式 | 作用 |
|---|---|---|
explore() | explore | 引导 Agent 侧重目录结构、README、关键模块 |
analyze() | analyze | 引导 Agent 查找 TODO、复杂度、缺测试等问题 |
plan_next_steps() | plan | 引导 Agent 回顾笔记并制定下一步计划 |
这里的mode不是硬编码流程,而是给 Agent 的方向性提示。真正的工具选择仍由 Agent 根据上下文和工具描述决定。
7.4 三天工作流
06_three_day_workflow.py把长程协作分成几个阶段:
| 阶段 | 目标 | 主要工具 | 产物 |
|---|---|---|---|
| 第一天:探索代码库 | 了解目录、模块、核心文件 | TerminalTool,NoteTool | 架构笔记、初步问题 |
| 第二天:质量分析 | 找重复代码、复杂函数、TODO、测试缺口 | TerminalTool,NoteTool | blocker、action |
| 第三天:规划重构 | 根据历史发现排序任务 | NoteTool,ContextBuilder | task_state、重构计划 |
| 一周后:复盘 | 检查进度并生成报告 | NoteTool | summary、report |
这个案例体现了上下文工程的几个关键能力:
- 跨会话连贯:前几天记录的问题能在后续规划中被召回。
- 上下文筛选:不是所有历史都进入模型,只拉取当前相关的笔记。
- 即时探索:需要具体文件时再用 TerminalTool 查看。
- 自动知识管理:重要发现写入笔记,避免丢失。
- 人机协作:人类可以手动新增、修改、审核计划笔记。
7.5 可扩展方向
这个案例还可以继续扩展:
- 接入
RAGTool,为代码库构建语义索引,用于概念级检索。 - 拆分探索者、分析者、规划者三个子代理,提升复杂任务并行度。
- 允许受控执行测试命令,把验证结果纳入笔记。
- 与 git 集成,记录变更、diff 和回滚点。
- 用 Gradio 或 Streamlit 做可视化任务看板。
8. 本章关键实践原则
- 上下文不是越多越好,而是越相关、越新、越结构化越好。
- 系统提示要稳定,但不能把复杂业务流程全部硬塞进提示。
- 工具集要小而清晰,工具返回值要面向 token 预算设计。
- 长程任务必须有外部状态层,不能只依赖对话历史。
- 即时访问适合动态、实时、精确的信息;RAG 适合稳定、语义化、可复用的知识。
- 压缩要保留决策、约束、未解决问题和下一步,而不是只追求短。
- 笔记必须定期整理,否则会从 “外部记忆” 变成 “外部噪声”。
- 高风险工具调用必须有安全边界和人类审批。
9. 运行建议
本章代码依赖.env中的 LLM 配置。README 里也给出了嵌入模型配置建议,快速测试可使用 TF-IDF:
import os os.environ["EMBED_MODEL_TYPE"] = "tfidf" os.environ["EMBED_MODEL_NAME"] = ""推荐运行顺序:
cd D:\Project\Agent\hello-agents-1.0.2\code\chapter9 # 先运行不强依赖 LLM 的工具示例 python 03_note_tool_operations.py python 05_terminal_tool_examples.py # 再看上下文构建和 Agent 集成 python 01_context_builder_basic.py python 02_context_builder_with_agent.py # 最后看完整长程工作流 python 06_three_day_workflow.py如果MemoryTool使用working之外的记忆类型,可能需要向量数据库或嵌入模型配置;codebase_maintainer.py中为了降低依赖,使用了memory_types=["working"]。
10. 习题解析
题 1:上下文工程与提示工程
1.1 什么是上下文腐蚀?为什么 100K/200K 窗口仍需管理?
上下文腐蚀是指上下文变长后,模型对关键信息的定位、回忆和使用能力下降。它不代表模型不能读长文本,而是代表长文本中的有效注意力会被更多 token 分散。
即使有 100K 或 200K 窗口,也要谨慎管理,原因是:
- 成本更高:输入 token 越多,费用和延迟越高。
- 噪声更多:过期信息、重复工具输出、无关历史会影响判断。
- 冲突更多:旧计划和新计划、旧代码和新代码可能互相矛盾。
- 精度下降:模型可能忽略中间位置的信息,或错误引用不相关片段。
所以长窗口是缓冲区,不是垃圾桶。工程上应优先放入高价值信息,并把低价值信息放到外部存储中按需检索。
1.2 代码审查助手:一次性加载 50 个文件 vs JIT 按需检索
| 策略 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|
| 一次性加载全部文件 | 实现简单,模型能直接看到全局内容,适合小代码库 | 成本高、噪声大、容易超限,文件更新后上下文过时 | 文件少、任务要求全局一致性、代码量可控 |
| JIT 按需检索 | 实时、低成本、聚焦当前问题,可处理大代码库 | 多轮工具调用更慢,需要探索策略,可能漏查 | 大代码库、日志分析、定位具体缺陷、持续维护 |
对 50 个文件的代码审查助手,推荐混合方案:先加载 README、目录结构、依赖配置和变更摘要,再通过find/grep/head按需查看关键文件。最后把审查发现写入 NoteTool,避免重复扫描。
1.3 系统提示的两个误区与平衡
过度硬编码示例:
如果用户问性能,就先查 A 文件;如果问数据库,就先查 B 文件; 如果出现 timeout,就必须回复固定模板……问题是流程脆弱,真实项目结构变化后容易误导 Agent。
过于空泛示例:
你是一个优秀的代码审查助手,请认真分析代码并给出建议。问题是缺少审查维度、工具使用规则、输出格式和风险等级标准。
平衡写法应包含稳定原则和可执行约束:
你是代码审查助手。优先关注正确性、安全、数据一致性、错误处理和测试缺口。 当缺少文件内容时,使用工具按需检索;不要假设未查看的代码。 输出按 P0/P1/P2 分级,每条包含文件位置、问题、影响和建议修复。题 2:GSSC 流水线
2.1 四个阶段失效的影响
| 阶段 | 失效表现 | 对 Agent 的影响 |
|---|---|---|
| Gather | 没取到关键记忆、RAG、笔记或工具结果 | 回答缺上下文,重复提问或错误假设 |
| Select | 选入无关信息,漏掉关键证据 | 注意力被噪声占据,判断偏离任务 |
| Structure | 信息混在一起,无分区和来源 | 模型分不清规则、证据、历史,调试困难 |
| Compress | 压缩过度或截断关键状态 | 长程任务断裂,遗漏约束、计划和风险 |
最危险的是 Select 和 Compress。前者决定模型看什么,后者决定超限时牺牲什么。两者都应有日志和评估指标。
2.2 为 ContextBuilder 添加上下文质量评估
可以在build()后增加evaluate_context_quality(),输出三个指标:
- 信息密度:非模板文本中有效内容占比,重复片段越多分数越低。
- 相关性:选中 packet 的平均相关性分数。
- 完整性:是否包含系统指令、当前任务、必要证据、历史状态、输出要求。
示例设计:
def evaluate_context_quality(context: str, packets: list[ContextPacket]) -> dict: avg_relevance = sum(p.relevance_score for p in packets) / max(len(packets), 1) total_tokens = sum(p.token_count for p in packets) unique_contents = {p.content.strip() for p in packets} duplication_rate = 1 - len(unique_contents) / max(len(packets), 1) sections = { "role": "[Role & Policies]" in context, "task": "[Task]" in context, "context": "[Context]" in context or "[Evidence]" in context, "output": "[Output]" in context, } completeness = sum(sections.values()) / len(sections) suggestions = [] if avg_relevance < 0.5: suggestions.append("相关性偏低,建议提高 min_relevance 或优化检索 query。") if duplication_rate > 0.3: suggestions.append("重复信息较多,建议去重或压缩历史工具输出。") if completeness < 1: suggestions.append("上下文分区不完整,检查系统指令、任务和输出要求。") return { "avg_relevance": avg_relevance, "token_count": total_tokens, "duplication_rate": duplication_rate, "completeness": completeness, "suggestions": suggestions, }这个功能最好和日志结合,记录每次构建时选中了哪些 packet、丢弃了哪些 packet、为什么丢弃。
2.3 截断、滑动窗口、LLM 摘要分别适合什么?混合压缩策略怎么设计?
简单截断更适合结构清楚、重要信息集中在开头的内容,例如 README 前言、命令输出前 N 行、错误栈顶部。滑动窗口适合关注最近状态的内容,例如对话最近几轮、日志最后 200 行。LLM 摘要适合长对话、研究过程、设计讨论,因为需要抽取决策和未解决问题。
混合压缩策略:
- 按信息类型分类。
- 工具输出先去重和截断,保留命令、关键结果和错误。
- 对话历史用滑动窗口保留最近几轮。
- 深历史用 LLM 摘要,保留决策、约束、TODO 和风险。
- 法规、合同、代码关键片段不轻易摘要,而是保留引用位置和原文片段。
- 压缩后重新做质量评估,确保关键字段未丢失。
题 3:NoteTool 和 TerminalTool 扩展
3.1 笔记自动整理机制
可以把笔记分为三层:
- 临时笔记
scratch:工具探索、中间发现、未经确认的问题。 - 任务笔记
task_state/action/blocker:当前阶段需要跟踪的状态。 - 项目笔记
conclusion/reference:长期有效的设计决策、架构事实和稳定知识。
自动整理流程:
触发条件: scratch 数量超过阈值,或任务阶段结束,或上下文构建频繁命中重复笔记 整理步骤: 1. 按 tags、文件路径、主题聚类。 2. 识别重复内容和已解决内容。 3. 将高价值发现提升为 blocker/action/conclusion。 4. 给每条提升笔记补充来源和时间。 5. 将低价值 scratch 归档或删除。 6. 生成人类可审阅的整理报告。关键是不要让 Agent 静默删除重要信息。自动整理应先生成草案,由人类审核或至少保留归档。
3.2 TerminalTool 安全机制是否足够?如何做人机审批?
当前白名单、路径验证、权限检查、超时和输出限制适合只读探索,但不足以安全支持敏感文件访问或危险操作。风险包括:
- 白名单命令也可能泄露敏感信息,例如
cat secrets.env。 grep可能大范围扫出密钥。sed/awk如果允许复杂表达式,可能产生非预期行为。- 写操作、测试执行、git 操作会改变系统状态。
人机协作审批流程可以这样设计:
| 风险等级 | 操作 | 处理方式 |
|---|---|---|
| 低 | ls,find,head查看普通项目文件 | 自动执行并记录日志 |
| 中 | 读取.env、密钥文件、用户数据、生产日志 | 先展示原因、路径、范围,请人类批准 |
| 高 | 写文件、删除文件、数据库变更、网络请求、部署 | 默认禁止,必须人工批准并限定参数 |
| 极高 | 删除目录、重置仓库、修改生产配置 | 不允许 Agent 自主执行 |
审批请求应包含:命令、工作目录、访问路径、目的、预期输出、潜在风险和回滚方案。审批结果也要写入 NoteTool,便于审计。
3.3 智能代码重构助手流程图
用户提出重构目标 -> ContextBuilder 构建初始上下文 -> TerminalTool 读取 README、目录结构、测试情况 -> NoteTool 写入 task_state: 初始理解 -> TerminalTool 定位目标模块和相关调用 -> Agent 分析风险和依赖 -> NoteTool 写入 blocker/action/conclusion -> 人类确认重构计划 -> 受控执行小步修改 -> 运行测试或静态检查 -> TerminalTool 收集结果 -> NoteTool 更新进度和问题 -> ContextBuilder 在下一轮只召回相关状态 -> 循环直到任务完成 -> 生成最终报告和遗留问题清单这里最重要的是小步推进和状态外部化。每个阶段都要有可恢复的笔记记录,不能把重构过程只留在对话历史中。
题 4:长时程任务管理案例
4.1 三层上下文如何协调?
| 信息类型 | 放在哪里 | 原因 |
|---|---|---|
| 当前要看的文件片段、命令输出 | TerminalTool -> 当前上下文 | 即时、局部、可能很快过期 |
| 最近几轮对话目标和偏好 | MemoryTool working | 支撑会话连贯 |
| 阻塞问题、任务计划、阶段结论 | NoteTool | 需要跨会话稳定保存 |
| 大量稳定文档、API 文档、知识库 | RAGTool | 适合语义检索和复用 |
| 每次模型调用的最终输入 | ContextBuilder | 负责筛选和编排 |
避免冗余和不一致的方法:
- 每类信息有唯一主存储:计划以 NoteTool 为准,短期对话以 MemoryTool 为准。
- 笔记需要状态字段,例如
open/resolved/archived。 - 重要结论写来源和更新时间。
- ContextBuilder 只拉取相关笔记,不把所有笔记都塞入上下文。
- 当 Memory 和 Note 冲突时,优先使用更新时间更新、类型更权威的信息。
4.2 断点续传机制
断点续传要记录足够恢复的信息:
type: task_state status: in_progress current_phase: refactor_order_service last_completed_step: add_characterization_tests next_step: extract_stock_check_method modified_files: - services/order_service.py - tests/test_order_service.py open_blockers: - note_20250119_153000_2 validation: last_test_command: pytest tests/test_order_service.py last_test_result: failed failure_summary: test_process_order_pending_stock恢复流程:
- 读取最新
task_state。 - 检查相关文件是否仍存在、是否有未提交改动。
- 读取 open blocker 和 action。
- 重新运行或查看最近验证命令结果。
- 让 Agent 生成 “恢复计划”,先请人类确认。
- 继续执行下一步。
验证恢复正确性的关键是对比笔记状态与真实环境:文件、测试、git diff、依赖版本都要重新确认,不能只相信笔记。
4.3 任务依赖管理系统
可以用 NoteTool 存储任务节点:
id: task_add_email_unique_constraint type: action status: pending priority: high depends_on: - task_check_duplicate_emails blocks: - task_run_user_migration estimated_effort: 0.5d owner: agent调度逻辑:
- 读取所有
action/task_state笔记。 - 构建依赖图。
- 检查环形依赖。
- 找出
depends_on全部完成的任务作为 ready 队列。 - 按优先级、风险、工作量排序。
- 执行或建议下一步。
- 更新任务状态并写回 NoteTool。
这样 NoteTool 不只是记录文本,而是变成轻量任务数据库。
题 5:渐进式披露
5.1 应用场景:复杂 bug 调试
以 “线上接口偶发 500” 为例:
第一步:读取最近错误日志 -> 发现错误集中在 /orders 第二步:查看 /orders 路由 -> 发现调用 OrderService.process_order 第三步:查看 process_order -> 发现库存检查和状态更新在同一函数 第四步:搜索相关 TODO 和最近提交 -> 发现有人改过库存字段类型 第五步:运行或查看测试 -> 发现缺少并发扣库存测试 第六步:记录 blocker 和修复计划Agent 每一步只看必要信息,后续探索由前一步结果决定。这比一次性加载全部日志、全部代码和全部历史提交更聚焦。
5.2 探索引导机制
为避免 Agent 在不重要细节上浪费时间,可以设计元认知检查:
- 每轮工具调用前回答:我为什么需要这个信息?它会改变哪个决策?
- 限制同类探索次数,例如连续 3 次 grep 没有新发现就换策略。
- 优先查看高信号文件:README、入口文件、配置、错误栈指向文件、测试文件。
- 为每个阶段设置信息目标,例如 “定位模块”“确认根因”“验证修复”。
- 定期生成探索摘要:已知事实、未知问题、下一步最高价值动作。
- 使用 NoteTool 记录已排除路径,避免重复探索。
可以给 Agent 一个简单策略:
每次探索后更新: 1. 已确认事实 2. 当前最可能假设 3. 仍缺的证据 4. 下一步最小代价验证动作5.3 渐进式披露 vs 一次性加载的适用任务
渐进式披露明显占优的任务:
- 大代码库调试:信息量大,相关文件需要逐步定位。
- 日志排障:最近错误、时间窗口、服务链路决定下一步查哪里。
- 学术综述:先看摘要和引用网络,再深入关键论文。
- 安全审计:从入口、权限、数据流逐层追踪。
一次性加载可能更合适的任务:
- 短文本改写:原文很短,整体风格一致性重要。
- 小型配置审查:几十行配置一次看完整更可靠。
- 合同条款对比:材料规模可控,且需要全局一致地检查交叉引用。
现实中最稳的仍是混合模式:先加载任务说明、目录或摘要等高层上下文,再通过渐进式方式获取细节。
11. 本章总结
第 9 章的重点不是新增一个炫技工具,而是建立一种工程习惯:每次调用模型前,都要有意识地决定模型该看到什么、不该看到什么、看到的信息如何分区、超限时如何保留关键状态。
ContextBuilder负责把上下文构建标准化,NoteTool负责把长期状态外部化,TerminalTool负责按需获取即时信息,MemoryTool负责保留会话记忆。它们组合起来,才真正支撑长程 Agent 在复杂任务中保持连贯、聚焦和可恢复。
