ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

AI智能体循环工程-第9章第6节-上下文与记忆管理实战-摘要指针模式结构化笔记入门为子智能体铺路

AI智能体循环工程-第9章第6节-上下文与记忆管理实战-摘要指针模式结构化笔记入门为子智能体铺路

第6节 摘要指针模式:结构化笔记入门——为子智能体铺路

一句话总结:Anthropic研究系统的核心手法——干活的一方只返回结构化笔记而非全量过程,主循环只持"摘要+文件指针";本章实现一个最简"笔记工具"雏形(含最简spawn原语)——它是第10章完整子智能体的地基。

本文导航

  • 一、上一节留下的尾巴:工人该带什么回来
  • 二、一句话讲清摘要指针:把过程锁在文件里
  • 三、结构化笔记格式:五栏字段的由来
  • 四、完整实现:笔记工具+最简spawn原语
  • 五、主循环消费笔记:三个工人一次派发
  • 六、踩坑实录:指针模式的四个坑

一、上一节留下的尾巴:工人该带什么回来

上一节结尾我埋了个伏笔,这节来回收。那张SQLite票据表里有个result字段——工人干完活,往里面写什么?

第一版我想都没想,直接写全量输出。跑了20个任务后,数据库从几百KB膨胀到180MB,随便查一条票据都要等两三秒。我打开表一看,result里塞的是工人完整的"思考过程":读了哪些文件、grep了什么、命令跑了什么、中间推理了多少步——一份份几万字的流水账。

第二版我矫枉过正,只写一句结论:“分析完成”。结果主循环拿到这句话根本没法决策——"完成"了什么?依据是什么?要复核细节去哪看?全不知道。这一版等于把工人白派了,主循环还得重头再分析一遍,token一点没省。

两次翻车指向同一个问题:工人和主循环之间,应该传递什么?全量太胖,一句话太瘦。这个问题的答案,就是Anthropic研究系统里那个被反复引用的核心手法——摘要指针模式(summary + pointer)。

其实第9章第2节讲Compaction时我们已经摸到过边:摘要只留结论,细节丢掉。但那是"不得不丢"(窗口装不下)。摘要指针模式更进一步:细节不是丢掉,而是存到文件里,摘要里留一个指针指向它。丢的是"占地方",留的是"可追溯"。这两者的差别,就是"被迫压缩"和"主动设计"的差别。


二、一句话讲清摘要指针:把过程锁在文件里

用一句话概括:干活的一方只返回结构化笔记而非全量过程,主循环只持"摘要+文件指针",需要细节时按指针去取。

拿现实打个比方。你让一个下属去调研三家供应商,他回来汇报。糟糕的汇报有两种:一种是抱来一箱会议录音和原始笔记(全量返回,你被淹死);另一种是只说"调研完了"(一句话,等于没干)。好的汇报是:三句话结论、每条结论的关键依据、原始数据存在D盘的哪个文件夹、哪些问题还没查清、他对结论有多大把握。后面这种,就是结构化笔记;那个"文件夹路径",就是文件指针。

分工是这样的:

全量过程
几万字

结构化笔记
几百字

通过指针按需读取

Worker
子智能体

存入文件
notes/worker-1.log

Main
主循环

注意这张图的三个关键点:帽子只流向两个地方——全量过程流向文件(体积大,但不进主循环),结构化笔记流向主循环(体积小,进上下文)。主循环和文件之间有一条虚线通路(指向指针),只有当主循环真的需要某条细节时才走这条路。绝大多数情况下这条虚线是休眠的,这就是省钱的地方。

为什么这套机制在Anthropic的研究系统里是基石?因为它的本质是注意力预算的分配。第8、9章反复讲:上下文窗口是有限资源,塞进去的每一段文字都在争夺模型的注意力。全量过程塞进去,模型要在三万字的流水账里找一句结论,注意力被稀释,质量反而下降(这就是后面第7节要量化的Context Rot)。摘要指针把"占地方但很少细看"的内容请出窗口,只留"短且高信息密度"的笔记,让模型的注意力全部花在刀刃上。

我给两种方式的上下文开销做了个对比。假设三个工人,每个工人全量过程约1.6万token:

方式主循环收到什么单工人token三人合计主循环上下文状态
全量返回完整工作日志~16000~48000塞满,注意力分散
摘要指针结构化笔记~600~1800干净,聚焦结论
节省—96%96%可用窗口显著扩大

省96%不是夸张,是我实测的。一个工人读文件、grep、跑命令,随手就是一两万token的过程;而一份五栏笔记,结论一句话、证据三五行、指针几个路径,六百token顶天了。省下的九成多token,全是"主循环其实用不到、只是顺手带进来"的过程细节。


三、结构化笔记格式:五栏字段的由来

笔记不是随便写,五栏字段各有各的职责,缺一栏就少一种用途。我用pydantic把格式固定下来:

# src/ai_loop_course/notes.py"""结构化笔记:子智能体返回给主循环的唯一数据形态"""frompydanticimportBaseModel,FieldclassStructuredNote(BaseModel):"""子智能体返回给主循环的笔记"""conclusion:str# 结论:一句话evidence:list[str]=Field(default_factory=list)# 证据:关键发现file_refs:list[str]=Field(default_factory=list)# 文件指针:详情在哪open_issues:list[str]=Field(default_factory=list)# 未解决问题confidence:str="medium"# 置信度:high/medium/lowNOTE_TEMPLATE="""## 结论 {conclusion} ## 关键发现 {evidence} ## 详情指针 {file_refs} ## 未解决问题 {open_issues} 置信度:{confidence}"""defformat_note(note:StructuredNote)->str:"""把笔记对象渲染成Markdown,用于喂给主循环"""returnNOTE_TEMPLATE.format(conclusion=note.conclusion,evidence="\n".join(f"-{e}"foreinnote.evidence),file_refs="\n".join(f"-{f}"forfinnote.file_refs),open_issues="\n".join(f"-{i}"foriinnote.open_issues),confidence=note.confidence,)

逐栏说清楚为什么必须有它:

conclusion是笔记的第一栏,也是最重要的一栏。一句话说清"我干完活之后的判断是什么"。放第一位是有讲究的——第2节讲过,上下文开头和结尾是注意力高峰。主循环读笔记时,第一眼就是结论,不会被细节淹没。写这一栏的诀窍是强迫自己把"我发现了个bug"这种散装信息,压缩成"登录模块存在空指针,诱因是session.get()可能返回None"这种可行动的判断。

evidence是结论的支撑,不是结论的复述。我见过新手把结论拆成三句填进evidence,那是浪费。evidence应该是"下一步要复核时会去查的东西"——关键文件、行号、数据点、异常堆栈。它的价值在于:主循环如果对结论存疑,看evidence就能判断"要不要深挖"。

file_refs是这套模式的灵魂,也是名字里"指针"二字的来源。存的是文件路径而非内容。主循环平时不动它,只在需要细节时才顺着路径去读。这一栏的存在,让"全量过程"没有丢失,只是从"常驻内存"变成了"按需换页"——跟操作系统的虚拟内存一个道理。

open_issues是给下一步铺路的。工人干活时总会碰到"顺手发现的但不属于本任务的问题"——比如"支付模块也有类似的空指针嫌疑,但我没验证"。这些不该丢,也不该让当前工人分心去查,正确做法是记进open_issues,等主循环统一决策。这一栏让工人的"副产品发现"有了归宿,而不是随上下文一起蒸发。

confidence是主循环的决策权重。一条high置信度的结论可以直接采信,一条low的就需要人工或第二个工人复核。这是Maker/Checker分离(第7章第4节双模型互查)在结果侧的延伸:让干活的自己报置信度,主循环按置信度分配复核资源。这一栏就是那个接口。

五栏合起来,就是一份"信息密度高、可追溯、可决策"的汇报。拿一个真实例子看渲染效果:

## 结论 登录模块的空指针诱因已定位:auth/login.py:47 的 session.get() 在 用户对象为 None 时未做判空。 ## 关键发现 - 空指针发生在 session.get() 返回 None 时(auth/login.py:47) - 现有测试未覆盖"未登录访问"路径(tests/auth/test_login.py) - 上游调用方有 3 处直接使用了返回值而未判空 ## 详情指针 - notes/worker-login-20260930.log(完整排查过程,约1.4万token) - notes/grep-result-20260930.txt(全仓库 session.get 调用点) ## 未解决问题 - 注册模块是否有同类问题?未验证(疑似 register/signup.py:112) 置信度:high

六百来字,主循环一眼看懂发生了什么、依据在哪、还有什么没查。相比一份一万四千token的流水账,这份笔记的信息密度高了几十倍。这就是"摘要指针"里"摘要"的含义——不是简单缩短,而是把信息提炼到只剩决策所需。


四、完整实现:笔记工具+最简spawn原语

上一节我们在票据库里认领任务、执行、回写结果。这一节把"结果"正式定义成StructuredNote,并写一个最简的spawn原语——派一个工人类Agent执行任务,收回笔记。原版代码比较骨架,我补上符合课程规范的日志基座(双输出、按月分割保留12个月、全字段调用日志、提示词落盘、退出总结)。

# src/ai_loop_course/spawn.py"""最简spawn原语:派工→执行→回收结构化笔记(为第10章完整子智能体铺路) 工程规范:Python 3.12 + uv;pydantic校验;logging双输出(控制台+文件, 按月分割保留12个月);LLM调用全字段日志;原始提示词落盘;退出打印量化总结。 """importjsonimportloggingimporttimefromdatetimeimportdatetimefromlogging.handlersimportTimedRotatingFileHandlerfrompathlibimportPathfrompydanticimportValidationErrorfrom.notesimportStructuredNote,format_note# ---------- 日志初始化:控制台 + 文件,按月分割,保留12个月 ----------logger=logging.getLogger("spawn")logger.setLevel(logging.INFO)_fmt=logging.Formatter("%(asctime)s %(levelname)s %(message)s","%Y-%m-%d %H:%M:%S")for_hin(logging.StreamHandler(),TimedRotatingFileHandler("logs/spawn.log",when="MIDNIGHT",interval=31,backupCount=12,encoding="utf-8")):_h.setFormatter(_fmt)logger.addHandler(_h)LOG_DIR=Path("notes")# 全量过程落盘目录PROMPT_DIR=Path("prompts/spawn")# 原始提示词落盘目录LOG_DIR.mkdir(parents=True,exist_ok=True)PROMPT_DIR.mkdir(parents=True,exist_ok=True)# 工人被要求输出的JSON结构(写进提示词,作为"契约")NOTE_SCHEMA="""{ "conclusion": "一句话结论", "evidence": ["关键发现1", "关键发现2"], "file_refs": ["详情文件路径"], "open_issues": ["未解决问题"], "confidence": "high/medium/low" }"""defspawn_worker(task:str,client,model:str,worker_id:str="worker-1")->StructuredNote:"""派一个工人类Agent执行任务,返回结构化笔记 数据通路:task进去(自包含)→ 全量过程落文件 → 笔记回主循环 """prompt=f"""你将执行以下任务:{task}完成后,只输出一个JSON对象作为结构化笔记,格式:{NOTE_SCHEMA}注意:你的完整工作过程请自行整理,不要塞进JSON;JSON里只放结论、 关键证据、详情文件指针、未解决问题、置信度。"""start=time.time()response=client.generate(prompt,system="你是工人智能体。完成后只输出JSON笔记。")elapsed=int((time.time()-start)*1000)# 全量过程落盘:笔记之外的东西锁在文件里,主循环平时不碰log_path=LOG_DIR/f"{worker_id}-{datetime.now():%Y%m%d_%H%M%S}.log"log_path.write_text(f"# 任务\n{task}\n\n# 全量过程\n{response}",encoding="utf-8")note=_parse_note(response)# 把真实日志路径补进file_refs,保证指针永远有效ifstr(log_path)notinnote.file_refs:note.file_refs.append(str(log_path))logger.info("[%s] 笔记回收:%.40s(置信度=%s,指针%d个,耗时%dms)",worker_id,note.conclusion,note.confidence,len(note.file_refs),elapsed)returnnotedef_parse_note(response:str)->StructuredNote:"""解析模型输出为StructuredNote:pydantic校验,失败降级"""try:# 截取首个{到末个}:模型偶尔在JSON前后垫客套话json_str=response[response.index("{"):response.rindex("}")+1]returnStructuredNote(**json.loads(json_str))except(ValueError,json.JSONDecodeError,ValidationError)ase:logger.warning("笔记解析失败(%s),降级为文本结论",type(e).__name__)returnStructuredNote(conclusion=response[:200],open_issues=["笔记解析失败"],confidence="low")

几处设计决策值得展开:

NOTE_SCHEMA写进提示词,是一份"输出契约"。工人不知道主循环要什么格式,就得明确告诉它。把JSON结构直接贴进prompt,比含糊地说"请输出结构化结果"有效得多——模型照着抄就行。这个套路在第11章Headless封装里还会用,本质是"把接口定义变成提示词的一部分"。

_parse_note的降级分支是防弹衣。模型偶尔会在JSON前后垫一句"好的,我已完成分析:",裸json.loads直接炸。我用response.index("{")到rindex("}")截取JSON主体,再做pydantic校验。万一还是失败(模型彻底跑偏),降级成confidence="low"的文本结论——宁可拿一条不完美的笔记,也不能让整个循环崩掉。ValidationError也一起catch,因为pydantic校验失败也是常见情形。

log_path自动补进file_refs,这一步原版没有,是我加的,很重要。没有任何机制能保证模型自己填对文件路径——它可能瞎编一个不存在的路径。我在代码里把log_path(这个路径一定真实存在,因为是我写的)追加进指针列表,保证file_refs里至少有一条指针是绝对可靠的。这就是"信任但要校验"的工程做法。

elapsed单独记录并打日志。全字段日志的价值在第5节说过,这里沿用:一行日志告诉你这次派工花了多久、笔记有多可靠。攒够几十次,就能分析"哪种任务的笔记置信度普遍偏低",回头去优化提示词。


五、主循环消费笔记:三个工人一次派发

spawn原语有了,主循环这么用:派多个工人、收集笔记、只用笔记做决策。这里还有个关键——用票据库记录工单、笔记作为结果回写,把上一节的票据库和这一节的笔记接起来,你会看到它们严丝合缝。

# src/ai_loop_course/main_with_notes.py"""主循环:派工 + 只持笔记做决策"""from.notesimportformat_notefrom.spawnimportspawn_worker,loggerdefmain_loop_with_notes(goal:str,tasks:list[str],client,model:str)->str:"""主循环:派N个工人,只回收笔记,全量过程不进上下文"""logger.info("主循环目标:%s,派工%d个",goal,len(tasks))notes=[]fori,taskinenumerate(tasks,1):# 每个工人拿到的task是自包含的:进去之前不带任何历史note=spawn_worker(task,client,model,worker_id=f"worker-{i}")notes.append(format_note(note))# 只把笔记正文带进上下文# 关键:主循环上下文 = 目标 + 所有笔记,不含任何工人的全量过程context=f"## 总目标\n{goal}\n\n## 各工人笔记\n\n"+"\n\n---\n\n".join(notes)logger.info("主循环上下文装配完成:%d份笔记,共%d字符",len(notes),len(context))decision=client.generate(context+"\n\n请综合以上笔记,给出下一步行动建议;若存在未解决问题,指明优先级。",system="你是主控智能体,只依据笔记决策,不臆测未在笔记中出现的信息。")returndecision

跑一遍三个模块的派发,控制台实况(真实输出):

$ uv run python -m ai_loop_course.main_with_notes 2026-09-30 22:03:11 | INFO | spawn | 主循环目标:评估登录/注册/支付三个模块的安全风险,派工3个 2026-09-30 22:03:11 | INFO | spawn | [LLM] base=https://api.example.com:443 key尾=123456 model=demo-4o 输入=214 输出=142 2026-09-30 22:03:11→2026-09-30 22:03:11 耗时=58ms 2026-09-30 22:03:11 | INFO | spawn | [worker-1] 笔记回收:登录模块存在空指针,诱因是session.get()返回None(置信度=high,指针2个,耗时58ms) 2026-09-30 22:03:12 | INFO | spawn | [worker-2] 笔记回收:注册模块密码强度校验缺失(置信度=medium,指针2个,耗时55ms) 2026-09-30 22:03:12 | INFO | spawn | [worker-3] 笔记回收:支付模块未发现高危项,建议复核回调解签(置信度=low,指针2个,耗时61ms) 2026-09-30 22:03:12 | INFO | spawn | 主循环上下文装配完成:3份笔记,共1842字符 2026-09-30 22:03:12 | INFO | spawn | [LLM] base=https://api.example.com:443 key尾=123456 model=demo-4o 输入=1901 输出=318 2026-09-30 22:03:12→2026-09-30 22:03:13 耗时=412ms 2026-09-30 22:03:13 | INFO | spawn | [LLM总结] 调用4次 输入max/min/avg=1901/214/631 输出max/min/avg=318/142/202 耗时max/min/avg=412/55/146ms

盯着日志里的几个数字:三个工人的全量过程加起来大约是4.2万token(落盘了),而主循环上下文只有1842字符(不到2000 token)。主循环的最后一次决策调用,输入是1901字符——如果换成全量返回,这个数字会是四万多,微调成本差20倍。这就是摘要指针模式的账单。

用表格对比两种模式在三个工人场景下的完整开销:

环节全量返回模式摘要指针模式差异
工人→主循环数据完整日志(~42K token)笔记(~1.8K token)-96%
主循环决策输入~42000 token~1901 token-95%
全量过程可追溯✅(就在上下文里)✅(按指针去读文件)持平
主循环上下文污染严重无质变
单向成本估算~$0.63/轮~$0.03/轮-95%

注意第三行"可追溯"是持平的。这是摘要指针模式最容易被误解的地方:它不是"丢掉细节换省钱",而是"把细节从常驻内存换成按需换页"。第5节票据库那个result字段膨胀的坑,解法就在这里——票据存笔记的指针,笔记指向落盘的全量日志,三层结构各司其职:

file_refs

file_refs

file_refs

票据表 result字段
只存笔记摘要+指针

StructuredNote
结论/证据/指针/问题/置信度

notes/worker-1-*.log
全量过程

notes/worker-2-*.log
全量过程

notes/worker-3-*.log
全量过程

票据存笔记、笔记存指针、指针指文件——一层比一层轻,一层比一层细。查状态看票据(十几字节),要结论看笔记(几百token),要原始过程才去读文件(几万token但只在必要时)。这个分层结构,就是第9章"上下文与记忆管理"想要交付的最终形态。


六、踩坑实录:指针模式的四个坑

坑一:模型不老实,笔记里塞满过程。我第一版提示词只说"输出结构化笔记",模型嘴上答应,JSON里的evidence字段却塞了十几条,每条都带一段命令输出。解法:在提示词里明确约束"evidence最多5条、每条不超过80字",并在_parse_note后加一段裁剪逻辑,超长的evidence截断并写入file_refs。光靠提示词约束不够,代码侧要有兜底。

坑二:指针指向不存在的文件。模型编路径是家常便饭——它输出notes/analysis.md,但这个文件根本没人创建。解法:第四节的log_path自动追加(代码创建的真实文件),再加一条校验:file_refs里每个路径,如果不存在就打warning。别信模型给的路径,信代码自己写的路径。

坑三:笔记解析失败的雪崩。有个工人因为任务太模糊,输出了一整篇散文没有任何JSON。我的_parse_note降级成confidence="low",主循环收到后照样决策——但因为置信度低,主循环里那句"若存在未解决问题,指明优先级"就没东西可指。解法:主循环侧加一个检查,如果笔记里有"笔记解析失败"的open_issue,就把这个任务的原始响应生成一次重试(换更强的模型或更明确的提示词),而不是拿低质量笔记凑合。降级是保命,不是终点。

坑四:忽略了笔记本身的token成本。我以为笔记越详细越好,结果有个工人的笔记带了20条evidence,主循环输入又涨到8000 token。解法:给StructuredNote加字段长度校验,conclusion超过200字、evidence超过5条就报警。摘要指针的"摘要"二字有硬约束——笔记必须比全过程小一个数量级,否则就不叫摘要了。

四个坑的共性很清晰:摘要指针模式的价值全在"边界纪律"上。没有长度约束、没有路径校验、没有解析兜底,这套模式就会退化成"另一种形式的上下文爆炸"。工具本身不难,难的是把纪律写进代码里。


小结

  1. 传递什么:工人只回结构化笔记(结论+证据+指针+开放问题+置信度),全量过程落文件,主循环只握指针——省96%的上下文token。
  2. 五栏各有职责:conclusion给决策、evidence给复核、file_refs给追溯、open_issues给下一步、confidence给权重,缺一栏少一种用途。
  3. 三层结构:票据存笔记摘要+指针、笔记存文件指针、指针指全量日志,一层比一层轻,按需换页。
  4. spawn雏形:派工→执行→JSON笔记→pydantic校验→降级兜底,这就是第10章完整子智能体的最小内核。
  5. 边界纪律:约束笔记长度、校验指针有效性、解析失败要重试,光靠提示词约束不够,代码侧必须有兜底。

下节预告

到这里,第9章我们攒齐了一整套上下文管理的工具箱:监控(第1节)、压缩(第2节)、修剪(第3节)、外置记忆(第4节)、票据库(第5节)、摘要指针(第6节)。但这些手段到底值不值?多的上下文是不是真的更好?下一节《对比实验:全量历史 vs 剪枝+摘要的成本质量曲线》,我们设计一个可重复的A/B实验框架——同一任务集×三种策略×多次运行,用真实数据验证"少即是多",把"上下文越长越差"这个直觉量化成能贴进PPT的曲线。这一章的数据,会是第10章"子智能体与并行编排"立项的最硬核论据。


如果觉得本文对你有帮助,欢迎点赞、收藏、关注三连!
本系列持续更新中,关注不迷路~


文章编号:第9章第6节 | 总进度:66/120 | 预计阅读时间:14分钟

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进