上下文完整性

一、问题

1.1 修复前的消息流

Turn 1: 用户"去成都3天"
  → supervisor 调用 LLM → 输出 PLAN|成都|3天|...
  → 4 个子 Agent 并行执行:
      route: 搜索景点 → LLM规划路线 → 存到 route_result(不可见!)
      hotel: 搜索酒店 → LLM对比分析 → 存到 hotel_result(不可见!)
      food:  LLM推荐餐厅 → 存到 food_result(不可见!)
      info:  LLM介绍文化 → 存到 info_result(不可见!)
  → aggregator 汇总 → 存 AIMessage("为用户规划了成都 3天行程")

  checkpoint.messages = [
      HumanMessage("去成都3天"),          ← supervisor 写入
      AIMessage("好的,成都 3天行程..."),  ← supervisor 写入
      AIMessage("为用户规划了成都 3天行程") ← aggregator 写入
  ]

问题:子 Agent 的具体产出——推荐了什么酒店、规划了什么路线、推荐了什么餐厅——全部封在 route_resulthotel_resultfood_resultinfo_result 这些状态字段中。这些字段不在 checkpoint 的 messages 列表里,LLM 在后续对话中看不到它们。

1.2 导致的体验断裂

用户:去成都3天
系统:好的,[输出成都3天行程 + 酒店 + 餐厅]

用户:把第二天的酒店换成便宜点的
系统:请问您之前看的是哪家酒店?  ← LLM 根本不知道推荐了什么!

LLM 的回复暴露了真相:它只知道"为用户规划了成都 3 天行程"这个笼统摘要,完全不知道具体推荐了哪几家酒店、价格如何。

这是一个上下文不对称问题:用户看到了完整结果(前端渲染了完整的行程卡片),但 LLM 看不到自己团队的工作成果。

二、根因分析

2.1 状态字段 ≠ 对话消息

LangGraph 的 checkpoint 同时保存两类数据:

类型 存储方式 LLM 能否看到
messages add_messages reducer → 追加到 checkpoint summarize_messages 处理后传给 LLM
route_result 普通字段,只被下一次 invoke_input 覆盖 看不到

summarize_messages 只处理 messages 列表,不关心 route_result 这种业务字段。子 Agent 把产出写到 result 字段,等于把信息锁进了 LLM 无法触及的抽屉里。

2.2 为什么一开始这么设计

最初的设计意图是"让 supervisor 统筹全局,子 Agent 只干活不发言"。这是一种人类管理直觉的映射:老板分配任务,员工交报告,老板汇总后向客户汇报。

但 LLM 不是人类经理——它不会主动翻阅"员工报告"。它只看自己对话窗口里的消息。不给它看报告,它就真不知道。

三、修复方案

3.1 每个子 Agent 写入摘要 AIMessage

修复后,每个子 Agent 在返回 result 的同时,也向 messages 写入一条精简的摘要:

# route_agent_node —— 路线摘要
day_summaries = ";".join(
    f"第{d['day']}天({' → '.join(a['name'] for a in d.get('activities',[]))})"
    for d in days_data
)
route_msg = f"已规划{dest} {days}天行程。{day_summaries}"
return {
    "route_result": json.dumps(result, ensure_ascii=False),
    "messages": [AIMessage(content=route_msg)],
}

# hotel_agent_node —— 酒店摘要
hotel_names = "、".join(h["name"] + f"({h['price']})" for h in hotels_json[:5])
hotel_msg = f"已筛选{dest}酒店:{hotel_names}。" + comparison
return {
    "hotel_result": ...,
    "messages": [AIMessage(content=hotel_msg[:300])],
}

# food_agent_node —— 美食摘要
food_lines = []
for fd in food_plan.get("days", []):
    lunch_name = (fd.get("lunch") or {}).get("name", "")
    dinner_name = (fd.get("dinner") or {}).get("name", "")
    food_lines.append(f"第{d}天 午餐:{lunch_name} 晚餐:{dinner_name}")
return {
    "food_result": ...,
    "messages": [AIMessage(content=food_msg[:300])],
}

# info_agent_node —— 文化摘要
info_msg = f"已整理{dest}旅行文化指南(含文化特色、冷知识、礼仪提醒等)"
return {
    "info_result": info_text,
    "messages": [AIMessage(content=info_msg)],
}

3.2 修复后的消息流

Turn 1: 用户"去成都3天"
  checkpoint.messages = [
      HumanMessage("去成都3天"),                      ← supervisor
      AIMessage("好的,成都 3天行程..."),              ← supervisor
      AIMessage("已规划成都3天行程。第1天(宽窄巷子 → 太古里 → 锦里)..."), ← route
      AIMessage("已筛选成都酒店:锦江宾馆(¥500)、如家(¥200)..."),      ← hotel
      AIMessage("已推荐成都3天美食。第1天 午餐:龙抄手 晚餐:蜀九香..."),  ← food
      AIMessage("已整理成都旅行文化指南..."),                        ← info
      AIMessage("为用户规划了成都 3天行程"),                         ← aggregator
  ]

LLM 现在能"看到"每个子 Agent 的具体产出。

3.3 关键设计决策:存摘要而非全量数据

为什么不直接把 route_result 的完整 JSON 写入 messages?

  • Token 消耗:完整 JSON 可能 2000+ tokens,5 轮对话后 checkpoint 严重膨胀
  • LLM 处理效率:LLM 不需要看到 JSON 结构——它只需要知道"推荐了哪些酒店、价格如何"
  • 摘要足够解决问题:用户说"换便宜的"时,LLM 看到"锦江宾馆(¥500)、如家(¥200)"就能判断哪个更便宜

摘要原则:存到 messages 里的信息要足够让 LLM 回答针对性的追问,但不要冗余到浪费 token

四、体验改善

4.1 修复前后对比

修复前:
用户:去成都3天
系统:[完整行程卡片]
用户:把第二天的酒店换成便宜点的
系统:请问您之前看的是哪家?          ← LLM 不知道推荐了什么

修复后:
用户:去成都3天
系统:[完整行程卡片]
用户:把第二天的酒店换成便宜点的
系统:您之前看的三家是锦江宾馆(¥500)、汉庭(¥300)、如家(¥200)。
      如家最便宜,但评分较低。建议换成汉庭,性价比最高。  ← LLM 能看到酒店信息

4.2 支持的追问类型

修复后,LLM 可以处理这些针对性的追问:

追问 LLM 需要的上下文 所在摘要
"换便宜的酒店" 酒店名称和价格 hotel 摘要
"去掉第三天的某个景点" 每天景点名称 route 摘要
"我不吃辣,换餐厅" 每天餐厅名称 food 摘要
"介绍下第二天那个景点" 景点名称 route 摘要

五、经验总结

  1. 对话记忆不只是"说了什么",还包括"产出了什么"。当一个 multi-agent 系统生成了结构化结果,把结果摘要写入对话记忆是让后续对话可理解的前提。

  2. 状态字段 ≠ LLM 可读。LangGraph 的 checkpoint 保存了完整的图状态,但 summarize_messages 只处理 messages 列表。子 Agent 的结果要进入 LLM 上下文,必须通过 messages 管道。

  3. 摘要优于全量。在对话记忆里存摘要而非原始 JSON,在 token 效率和可用性之间取得平衡。LLM 不需要看到 { "days": [{ "day": 1, "activities": [{...}] }] } 这种嵌套结构——它只需要知道"第一天去了宽窄巷子、太古里"。

  4. 与上一篇的关联:上一篇(多轮对话纠错)修复了 HumanMessage 缺失导致的"用户说了什么"丢失。本篇修复了子 Agent AIMessage 缺失导致的"系统产出了什么"丢失。两者共同构成了完整的对话上下文。

导航

← 上一篇:用户隔离设计