Agent Prompt 工程设计

一、问题诊断:SystemMessage 形同虚设

先看当前 4 个子 Agent 的 prompt 构造:

# 现状:SystemMessage 只定义了角色名(1句话)
ROUTE_SYSTEM_PROMPT = "你是资深旅行路线规划师。根据提供的景点信息,规划详细的每日行程路线。返回结构化JSON。"

# 现状:HumanMessage 承载了全部真实指令(30行)
plan_prompt = f"""你是资深旅行路线规划师。为{dep_part}{dest} {days}天行程规划详细路线。
要求:
1. 每天只安排 2-3 个景点,宁少勿多...
6. 景点游览时间要合理...
可用景点:{poi_text}
返回JSON:...
只返回JSON。"""
┌─────────────────────────────────────────────────┐
│              当前 prompt 结构                      │
│                                                  │
│  SystemMessage: "你是XX专家。做XX事。"(1句)     │
│  HumanMessage:  ← 所有真正的指令都在这里!        │
│    1. 角色再次声明(与 System 重复)              │
│    2. 详细规则(6条)                             │
│    3. 输入数据(POI列表/酒店数据)                │
│    4. 输出格式(JSON schema)                     │
│    5. 约束("只返回JSON")                        │
└─────────────────────────────────────────────────┘

这是 SystemMessage 与 HumanMessage 的职责颠倒

消息语义学

LangChain / LLM 训练中,三种消息有明确的语义层级:

消息类型 语义层级 用途
SystemMessage 最高优先级 定义角色、设定规则、约束行为。LLM 将其视为"必须遵守的指令"
HumanMessage 正常优先级 用户的具体请求。LLM 视为"需要回应的内容"
AIMessage 正常优先级 助手的回复

当指令放在 HumanMessage 中时,LLM 可能将其视为"建议"而非"规则"——这解释了为什么有时 agent 不按 JSON 格式返回(它把格式要求当作建议,而不是必须遵守的系统指令)。


二、逐个 Agent 分析

2.1 路线规划 Agent

当前调用:

resp = invoke_with_retry(route_agent, [
    SystemMessage(content=ROUTE_SYSTEM_PROMPT),   # 1句话
    HumanMessage(content=plan_prompt)              # 30行!
])

问题清单:

  1. HumanMessage 第一句 你是资深旅行路线规划师 与 SystemMessage 重复
  2. 6 条规划规则("每天2-3个景点"、"同区域"等)是固定规则,应该放在 SystemMessage
  3. JSON schema 是固定格式要求,应该放在 SystemMessage
  4. 只返回JSON输出约束,放在 SystemMessage 中更有效
  5. 只有景点列表 {poi_text} 和目的地 {dest} 才是真正的变量数据,应该放在 HumanMessage

2.2 酒店对比 Agent

当前调用:

compare_resp = invoke_with_retry(hotel_agent, [
    SystemMessage(content=HOTEL_SYSTEM_PROMPT),    # 1句话
    HumanMessage(content=compare_prompt)            # 酒店数据 + 指令
])

问题清单:

  1. 只返回对比分析文字,不超过100字 是输出约束,应该在 SystemMessage
  2. 用一段话对比分析 是格式要求,应该在 SystemMessage
  3. 只有酒店数据和预算才是变量数据

2.3 美食推荐 Agent

当前调用:

resp = invoke_with_retry(food_agent, [
    SystemMessage(content=FOOD_SYSTEM_PROMPT),     # 1句话
    HumanMessage(content=prompt)                    # 18行
])

问题清单:

  1. HumanMessage 第一句 你是{dest}本地美食达人 与 SystemMessage 重复,且混入了变量 {dest}
  2. 5 条要求中,必须是真实存在的知名餐厅给出招牌菜和人均消费包含不同价位 是固定规则
  3. 每天推荐午餐和晚餐各一家 部分依赖 days(变量)
  4. JSON schema 和 只返回JSON 应该在 SystemMessage

2.4 文化讲解 Agent

当前调用:

response = invoke_with_retry(info_agent, [
    SystemMessage(content=INFO_SYSTEM_PROMPT),     # 1句话
    HumanMessage(content=prompt)                    # 6行
])

相对较好——内容要求(文化特色、冷知识、礼仪提醒)确实是每次不同的"话题要求"。但 用中文,语言轻松有趣,不要百科式罗列 是风格约束,属于 SystemMessage 范畴。


三、子 Agent 上下文是否需要保存?

3.1 核心论证

supervisor:
  ✓ 需要跨请求对话记忆 → 写入 checkpoint.messages
  ✓ 需要摘要 → 使用 summarize_messages
  ✓ 需要知道"用户之前说过什么"

子 Agent (route/hotel/food/info):
  ✗ 不需要跨请求记忆 → 不写入 checkpoint.messages
  ✗ 不需要摘要
  ✗ 只需要当前 state 中的参数(destination/days/budget)

3.2 为什么子 Agent 不保存上下文?

论证 1:子 Agent 是无状态的单次任务

请求1: route_agent(destination="成都", days=3, pois=[...]) → route_result="..."
请求2: route_agent(destination="杭州", days=2, pois=[...]) → route_result="..."

请求2 不需要知道请求1 规划了什么

每个请求的输入(destination, days, pois)完全由 supervisor 从 state 中提取,子 Agent 是纯函数式调用:f(input) → output

论证 2:保存子 Agent 上下文会污染 checkpoint

如果 route_agent 的 SystemMessage(30行) + HumanMessage(30行) + 生成的 AIMessage 都存入 checkpoint:

每次旅行规划产生:
  route 调用: ~2000 tokens
  hotel 调用: ~800 tokens
  food 调用:  ~1500 tokens
  info 调用:  ~1000 tokens
  合计:       ~5300 tokens/次请求

用户对话 5 轮 → checkpoint 膨胀到 ~26,500 tokens
这些内容对后续对话毫无价值(因为每次的目的地、天数都不同)

论证 3:当前实现已经是正确的

def route_agent_node(state):
    # 从 state 读取输入参数
    dest = state.get("destination", "")
    days = state.get("days", 3)
    # ... 调用 LLM ...
    # 只写 result 字段,不写 messages
    return {"route_result": json.dumps(result)}
    # ✓ 正确的设计

结论:不做改变。子 Agent 不保存上下文是深思熟虑的设计决策,不是遗漏。


四、改进方案

4.1 设计原则

┌─────────────────────────────────────────────────┐
│  SystemMessage(系统指令层)                      │
│  ┌─────────────────────────────────────────────┐│
│  │ 1. 角色定义   "你是一个XXX专家..."           ││
│  │ 2. 输出格式   "必须返回以下JSON结构..."       ││
│  │ 3. 行为规则   "每天2-3个景点、同区域..."      ││
│  │ 4. 约束条件   "只返回JSON,不返回解释"        ││
│  └─────────────────────────────────────────────┘│
│                                                  │
│  HumanMessage(任务实例层)                       │
│  ┌─────────────────────────────────────────────┐│
│  │ 1. 任务描述   "为成都规划3天行程..."          ││
│  │ 2. 输入数据   POI列表 / 酒店数据 / 预算       ││
│  │ 3. 动态参数   目的地、天数、出发地等           ││
│  └─────────────────────────────────────────────┘│
└─────────────────────────────────────────────────┘

4.2 路线规划 Agent:改进后

ROUTE_SYSTEM_PROMPT = """你是资深旅行路线规划师。

## 角色
你负责根据提供的景点信息,规划详细的每日行程路线。

## 规划规则
1. 每天只安排 2-3 个景点,宁少勿多,留出休息和用餐时间
2. 同一天的景点必须在同一区域,步行或短途交通可达
3. 第一天考虑到达时间(如高铁/飞机),不要安排太满
4. 最后一天考虑返程时间
5. 为每两个景点之间给出交通方式和预计耗时
6. 景点游览时间要合理(大型景区3-4小时,小型1-2小时)

## 输出格式
必须严格返回以下JSON结构,不要包含任何JSON之外的内容:
{
  "dest_desc": "一句话描述",
  "transport_tip": "出发地到目的地的交通建议",
  "days": [
    {
      "day": 1,
      "theme": "当天主题",
      "activities": [
        {"time": "09:00-12:00", "name": "景点名", "duration": "3小时", "highlight": "亮点"}
      ],
      "transport_between": [
        {"from": "A", "to": "B", "mode": "步行/公交/打车", "time": "15分钟", "cost": "约10元"}
      ]
    }
  ]
}"""

# HumanMessage 只包含任务实例 + 数据
route_task = f"请为{dest}规划{days}天行程。可用景点:\n{poi_text}"

改进点:

  • SystemMessage 包容了角色、规则、格式——LLM 将其视为"必须遵守的指令"
  • HumanMessage 只传递"做什么"和"有什么数据可用"
  • 不再重复声明角色

4.3 酒店对比 Agent:改进后

HOTEL_SYSTEM_PROMPT = """你是酒店对比分析专家。

## 角色
你根据提供的酒店数据,对比分析并给出性价比推荐。

## 分析要求
1. 用一段话完成对比分析,语言简洁
2. 必须同时覆盖低价、中价、高价三个价位的酒店
3. 推荐时必须说明理由(位置好 / 评分高 / 价格优)
4. 考虑用户的预算偏好

## 输出格式
只返回一段中文对比分析文字,不超过150字,不包含任何JSON或格式化内容。"""

# HumanMessage 只包含数据
hotel_task = f"目的地:{dest}\n预算参考:{budget}\n待分析酒店:\n{json.dumps(hotels_json, ensure_ascii=False)}"

4.4 美食推荐 Agent:改进后

FOOD_SYSTEM_PROMPT = """你是本地美食达人,熟悉各地真实存在的知名餐厅。

## 角色
你为旅行者推荐每日餐厅,确保推荐的餐厅真实存在。

## 推荐规则
1. 必须是当地真实存在的知名餐厅,不能编造
2. 每天推荐午餐和晚餐各一家
3. 考虑餐厅位置与景点区域的匹配
4. 给出招牌菜和人均消费
5. 覆盖不同价位:街边小吃、特色餐馆、老字号

## 输出格式
必须严格返回以下JSON结构:
{
  "food_culture": "一句话描述当地美食特色",
  "days": [
    {
      "day": 1,
      "lunch": {"name": "店名", "signature": "招牌菜", "price": "人均50", "area": "区域", "tip": "小贴士"},
      "dinner": {"name": "店名", "signature": "招牌菜", "price": "人均80", "area": "区域", "tip": "小贴士"}
    }
  ]
}"""

food_task = f"请为{dest} {days}天行程推荐每日餐厅。"

4.5 文化讲解 Agent:改进后

INFO_SYSTEM_PROMPT = """你是旅行文化讲解员,擅长用生动有趣的故事化语言介绍目的地。

## 风格要求
1. 用故事化的语言,不要百科式罗列
2. 语言轻松有趣,让读者有身临其境的感觉
3. 用中文
4. 内容要有深度但不枯燥

## 内容框架
请按以下结构组织:
1. 文化特色(2-3段)
2. 3个趣味冷知识
3. 实用礼仪提醒(用"场景-该做-不该做"格式)
4. 最佳旅行季节和注意事项"""

info_task = f"请介绍{dest}的旅行文化指南。"

五、改进前后对比

5.1 Prompt 结构

改进前:
┌──────────────────────────────────────────┐
│ SystemMessage: "你是XX专家。做XX事。"     │  ← 1句话,无约束力
│ HumanMessage: [角色] [规则] [格式] [数据] │  ← 30行,什么都有
└──────────────────────────────────────────┘

改进后:
┌──────────────────────────────────────────┐
│ SystemMessage:                           │
│   ## 角色 → 定义身份                     │
│   ## 规则 → 约束行为(LLM强行遵守)       │
│   ## 输出格式 → JSON Schema              │
│ HumanMessage: [任务描述] [动态数据]       │  ← 简洁,只传变量
└──────────────────────────────────────────┘

5.2 Token 效率

以路线规划 Agent 为例,假设用户连续 3 次调整行程:

改进前:
  每次 HumanMessage = ~800 tokens(固定规则+格式+数据)
  3次合计 SystemMessage tokens = 3 × 15 = 45 tokens
  3次合计 HumanMessage tokens = 3 × 800 = 2400 tokens

改进后:
  每次 SystemMessage = ~350 tokens(固定规则+格式,但可以缓存!)
  每次 HumanMessage = ~200 tokens(只有变量数据)
  3次合计 = 350 + 3 × 200 = 950 tokens  ← 节省 60%

注:DeepSeek 支持 prompt cache,SystemMessage 中固定的角色/规则/格式部分可以被缓存命中。如果混在 HumanMessage 里,每次不同的变量导致无法缓存。

5.3 指令遵守率

改进前:
  "只返回JSON" 在 HumanMessage 中 → LLM 视为"建议"
  → 有时返回 "好的,这是您要的JSON:\n{...}"
  → 需要代码手动提取 { 到 } 之间的内容

改进后:
  "只返回JSON" 在 SystemMessage 中 → LLM 视为"规则"
  → 更大概率直接返回纯 JSON
  → 减少后处理逻辑

六、子 Agent 上下文管理:最终结论

6.1 不作为

子 Agent 的 conversation (SystemMessage + HumanMessage + AIMessage)
不写入 checkpoint.messages

理由:
1. 子 Agent 是无状态纯函数:f(input_params) → result
2. 每次调用的输入参数完全不同(不同目的地、天数)
3. 保存会污染 checkpoint,对后续对话无价值
4. 当前实现已经是正确的

6.2 但需要保留的

子 Agent 的 result 字段必须保留在 checkpoint 中:
- route_result: 聚合器需要用它生成最终方案
- hotel_result: 同上
- food_result: 同上
- info_result: 同上

这些字段通过 StateGraph 的默认 reducer(覆盖)自动管理。
每次新请求时,旧的 result 被新的覆盖——这恰好是正确的行为。

6.3 一个例外:如果未来需要"参考上次方案"

如果未来需求要求子 Agent 参考上次的规划结果(例如:"在上次的路线基础上,把第二天改成..."),不应该通过保存子 Agent 对话来实现,而应该:

方案 A(推荐):由 supervisor 从 checkpoint 中提取上次的 result,
              作为上下文注入到本次的 HumanMessage 中
              "上次路线方案:{last_route_result}。请在此基础上调整..."

方案 B:将 result 字段改为 add_messages reducer(追加不覆盖),
        子 Agent 读取历史 result 作为参考

这保持了子 Agent 的无状态性,同时支持上下文感知。


七、实施建议

7.1 改动范围

Agent SystemPrompt 改动 HumanMessage 改动 风险
Route 重写(+300 chars) 精简(-600 chars) 中:JSON 解析可能受影响
Hotel 重写(+200 chars) 精简(-100 chars)
Food 重写(+300 chars) 精简(-400 chars) 中:JSON 解析可能受影响
Info 重写(+200 chars) 精简(-50 chars)

7.2 实施顺序

1. Info Agent  → 风险最低,先练手
2. Hotel Agent → 改动最小
3. Food Agent → 验证 SystemMessage 是否能约束 JSON 输出
4. Route Agent → 改动最大,最后改

7.3 验证方法

每次改动后验证:

  1. JSON 解析成功率(理想 >95%)
  2. 输出内容质量(人工抽查 3-5 个不同目的地)
  3. 不需要代码层面的兜底逻辑变更

八、核心要点

  1. SystemMessage 是"法律",HumanMessage 是"请求"——把规则和格式约束放在 SystemMessage,LLM 更倾向于遵守
  2. 每个子 Agent 的 SystemPrompt 要包含完整的角色、规则、格式、约束——而不是一行简介
  3. HumanMessage 只传变量数据——目的地、天数、POI 列表、预算等任务实例参数
  4. 子 Agent 对话不保存到 checkpoint——它们是无状态纯函数,这是正确的设计决策
  5. SystemMessage 中的固定内容可被 prompt cache 命中——这是分离固定/变量的实际收益

导航

← 上一篇:从 Checkpoint 到 State Sync | → 下一篇:多轮对话纠错