课程大纲 › 领域 5:上下文管理与可靠性

5.1 在长对话中管理上下文以保留关键信息

タスクステートメント5.1:長いやり取りを通じて重要な情報を保持するために会話コンテキストを管理する

官方大纲原文(日文):知识 4 条 · 技能 6 条

需要掌握的知识

  • 段階的な要約のリスク:数値、パーセンテージ、日付、顧客が述べた期待が曖昧な要約に圧縮されてしまうこと
  • 「lost in the middle」効果:モデルは長い入力の冒頭と末尾の情報は確実に処理するが、中間部分の指摘事項を見落とす場合があること
  • ツールの結果がコンテキストに蓄積し、関連性に見合わない量のトークンを消費する仕組み(例:関連するのは5個だけなのに、注文照会ごとに40+個のフィールドが返される)
  • 会話の一貫性を維持するために、後続のAPIリクエストで完全な会話履歴を渡すことの重要性

需要能做到的事

  • 取引に関する事実(金額、日付、注文番号、ステータス)を、要約された履歴の外に置かれ各プロンプトに含まれる永続的な「ケースの事実情報」ブロックに抽出する
  • 複数の問題を扱うセッションのために、構造化された問題データ(注文ID、金額、ステータス)を抽出して別のコンテキストレイヤーに永続化する
  • 冗長なツール出力がコンテキストに蓄積する前に、関連するフィールドのみに切り詰める(例:注文照会から返品に関連するフィールドのみを残す)
  • 位置による影響を軽減するために、集約した入力の冒頭に主要な指摘事項の要約を配置し、詳細な結果を明示的なセクション見出しで整理する
  • 下流での正確な統合を支えるために、サブエージェントに対して構造化出力にメタデータ(日付、情報源の場所、方法論上のコンテキスト)を含めることを要求する
  • 下流のエージェントのコンテキスト予算が限られている場合に、冗長なコンテンツや推論チェーンではなく構造化データ(主要な事実、引用、関連性スコア)を返すように上流のエージェントを変更する

课程讲解 · AI 草稿,未经人工审核 以官方文档为准

这一条考什么

这一条考你在长对话、多轮工具调用的场景里,怎么让关键信息(金额、日期、订单号、客户提出的期望)不在摘要或上下文膨胀中丢失。你要知道渐进式摘要的风险、"lost in the middle"效应、工具结果在上下文里堆积的问题,以及每次请求都要传完整历史的原因。你还要会用"案件事实块"、裁剪工具输出、调整信息位置、要求上游返回结构化数据等手段来应对。

核心概念

Messages API 是无状态的:每次都要传完整历史

Claude 的 Messages API 不会在服务端记住上一轮对话。每次请求都必须把需要的历史(之前的 user / assistant 消息、tool_use 和 tool_result)放进 messages 里传过去。如果应用只发送最新一条用户消息,模型就会"忘记"客户刚才已经提供过的验证信息,表现得前言不搭后语。

这类问题和提示词里有没有写"请记住"无关,也不是模型记忆力差,而是请求里根本没带上历史。

渐进式摘要的风险

对话越来越长时,常见的做法是把早期轮次压缩成摘要。问题是,每压缩一次都会丢失精度:

  • "客户 3 月 14 日下单,订单号 #88213,金额 $847.20,期望周五前到货"
  • 压一次变成"客户最近下了一个订单,金额较大,希望尽快到货"
  • 再压一次变成"客户有订单问题"

数字、百分比、日期、客户明确说出的期望,恰恰是后面处理问题最需要的,也最容易被压成模糊的叙述。

案件事实块(Case Facts):把关键事实放在摘要之外

解决办法是把交易类事实抽取出来,单独放在一个持久的结构化块里,每次请求都原样带上,永远不参与摘要压缩:

## CASE FACTS(每轮原样注入,不得摘要)
- customer_id: C-20931
- issues:
  - id: ISSUE-1 | order: #88213 | amount: 847.20 USD | status: refund_pending | customer_expectation: 周五前退款到账
  - id: ISSUE-2 | order: #88507 | amount: 32.00 USD | status: shipping_delay | customer_expectation: 改为加急

对话的叙述部分可以摘要,事实块则逐字保留。一次会话同时处理多个问题时,按问题分别记录结构化数据(订单 ID、金额、状态),作为独立的一层上下文持久化。这样客户突然提到"刚才那个运费的问题",Agent 也能准确对上。

和其他方案比较:滑动窗口只保留最近 N 轮,早期的事实会直接丢掉;把早期轮次全部写成叙述性摘要会丢精度;靠工具按需重新查询,又拿不到客户说过但系统里没有的期望。

工具结果在上下文中堆积

工具返回的内容会原样进入上下文,并且在之后的每一轮都要占用 token。一次订单查询可能返回 40 多个字段(仓库编码、内部 SKU 映射、物流节点……),而处理退货只需要其中 5 个。查询 10 次之后,上下文里大部分都是噪声。

做法是在工具结果进入上下文之前就裁剪:在工具实现里或 PostToolUse hook 里只保留和当前任务相关的字段。

RETURN_FIELDS = {"order_id", "status", "total", "purchase_date", "return_eligible_until"}

def lookup_order_for_return(order_id):
    raw = backend.get_order(order_id)
    return {k: raw[k] for k in RETURN_FIELDS if k in raw}

"Lost in the middle"效应

对于很长的输入,模型对开头和结尾的信息处理得最可靠,夹在中间的内容更容易被忽略。所以把多个子 Agent 的结果汇总给下游时:

  • 在输入开头放一段关键发现摘要;
  • 详细结果用明确的分节标题组织("## 来源 A:财务数据"、"## 来源 B:新闻"),方便模型定位;
  • 最重要的指令或问题可以放在末尾再强调一次。

上游返回结构化数据,而不是冗长内容

当下游 Agent 的上下文预算有限时,上游(子 Agent)应该返回结构化数据:关键事实、引用、相关性分数、元数据(日期、来源位置、方法论背景),而不是大段原文或完整的推理链。

{"findings": [
  {"fact": "2024 年欧洲热泵销量下降 21%", "source": "EHPA 年报 p.12", "date": "2025-03",
   "method": "17 国销售数据汇总", "relevance": 0.9}
]}

元数据能让下游做出准确的整合:知道日期才能区分新旧数据,知道方法才能解释数据之间的差异(见 5.6)。

正确做法与反模式

  • 正确:把金额、日期、订单号、状态抽取到案件事实块中,每轮原样注入。反模式:把整个早期对话压成一段叙述性摘要。
  • 正确:多问题会话里,每个问题单独记录结构化数据,作为单独的一层上下文。反模式:只保留最近 30 轮的滑动窗口。
  • 正确:工具输出进入上下文前,只保留与任务相关的字段。反模式:把 40 多个字段原样塞进历史,让它们在每一轮都占用 token。
  • 正确:汇总输入的开头放关键发现摘要,正文用明确的分节标题。反模式:把所有结果按到达顺序平铺,重要发现埋在中间。
  • 正确:要求子 Agent 返回带日期、来源和方法的结构化数据。反模式:上游返回完整推理链和大段原文,把下游的上下文挤爆。
  • 正确:每次 API 请求都传完整的对话历史(或者"事实块 + 摘要 + 近期轮次")。反模式:只发送最新一条消息,然后在提示词里写"请记住之前的内容"。

常见陷阱

  • "模型记不住之前的内容,因为提示词没要求它记住":真正的原因是请求里没带历史。API 是无状态的。
  • "用滑动窗口保留最近 N 轮":早期的关键事实会直接被丢掉。只要题干强调"客户之后会提到早期的问题",就应该选结构化事实层。
  • "把早期对话总结成叙述":数字和日期会被压得模糊不清。正确选项一般包含"抽取结构化事实并持久化"。
  • "依赖工具按需重新查询":查得到系统里的数据,但查不到客户说过的期望和对话中达成的共识,还会额外增加延迟和调用次数。
  • "换更大上下文窗口的模型":堆积和 lost in the middle 不会因此消失,只是推迟了。根本办法是裁剪和结构化。
  • "让上游返回完整原文,保证不丢信息":下游预算有限时这反而有害,应该返回结构化事实加引用。

实操练习

目标:实现一个"事实块 + 裁剪工具输出 + 完整历史"的客服循环,验证模型在多轮之后仍能准确引用金额和订单号。

  1. 写一个模拟的订单查询工具,返回 40 多个字段;外面再包一层只保留 5 个字段的函数。
  2. 维护一个 case_facts 字典,每轮从工具结果里更新,并拼进 system prompt。
  3. 进行 3 轮对话,第 3 轮问"我第一个订单的金额是多少?"。
import json
import anthropic

client = anthropic.Anthropic()

def raw_lookup(order_id):
    data = {f"internal_field_{i}": "x" * 20 for i in range(40)}
    data.update({"order_id": order_id, "status": "delivered", "total": 847.20,
                 "purchase_date": "2026-03-14", "return_eligible_until": "2026-04-13"})
    return data

KEEP = {"order_id", "status", "total", "purchase_date", "return_eligible_until"}
case_facts = {"customer_id": "C-20931", "issues": {}}

tools = [{"name": "lookup_order", "description": "按订单号查询订单(返回退货相关字段)",
          "input_schema": {"type": "object", "properties": {"order_id": {"type": "string"}}, "required": ["order_id"]}}]
history = []

def system_prompt():
    return "你是客服 Agent。\n## CASE FACTS(权威事实,原样引用)\n" + json.dumps(case_facts, ensure_ascii=False, indent=1)

def turn(user_text):
    history.append({"role": "user", "content": user_text})
    while True:
        r = client.messages.create(model="claude-sonnet-4-5", max_tokens=800, system=system_prompt(),
                                   tools=tools, messages=history)
        history.append({"role": "assistant", "content": [b.model_dump(exclude_none=True) for b in r.content]})
        if r.stop_reason != "tool_use":
            return "".join(b.text for b in r.content if b.type == "text")
        results = []
        for b in r.content:
            if b.type == "tool_use":
                trimmed = {k: v for k, v in raw_lookup(b.input["order_id"]).items() if k in KEEP}
                case_facts["issues"][trimmed["order_id"]] = trimmed
                results.append({"type": "tool_result", "tool_use_id": b.id, "content": json.dumps(trimmed)})
        history.append({"role": "user", "content": results})

print(turn("我想退订单 #88213"))
print(turn("另外 #88507 的物流也很慢"))
print(turn("我第一个订单的金额和退货截止日是多少?"))
print("history 中的 tool_result 字段数:", [len(json.loads(c["content"])) for m in history if m["role"] == "user" and isinstance(m["content"], list) for c in m["content"]])

要检查的结果:第 3 轮应该准确回答 847.20 和 2026-04-13;最后打印的字段数都应该是 5,而不是 45。试着把 KEEP 过滤去掉再跑一遍,对比 r.usage.input_tokens 的增长。

自测清单

  • 为什么每次 API 请求都要带上完整的对话历史?不带会出现什么现象?
  • 渐进式摘要最容易丢掉哪几类信息?案件事实块是怎么解决这个问题的?
  • 工具输出应该在哪个环节裁剪?为什么要在进入上下文之前就裁剪?
  • 什么是"lost in the middle"?汇总输入时怎样安排信息位置?
  • 下游上下文预算有限时,上游 Agent 应该返回什么?为什么要带上日期、来源、方法等元数据?

对应题目(3)

题目归类由 AI 判断,仅供参考。

练习这 3 道题