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行!
])
问题清单:
- HumanMessage 第一句
你是资深旅行路线规划师与 SystemMessage 重复 - 6 条规划规则("每天2-3个景点"、"同区域"等)是固定规则,应该放在 SystemMessage
- JSON schema 是固定格式要求,应该放在 SystemMessage
只返回JSON是输出约束,放在 SystemMessage 中更有效- 只有景点列表
{poi_text}和目的地{dest}才是真正的变量数据,应该放在 HumanMessage
2.2 酒店对比 Agent
当前调用:
compare_resp = invoke_with_retry(hotel_agent, [
SystemMessage(content=HOTEL_SYSTEM_PROMPT), # 1句话
HumanMessage(content=compare_prompt) # 酒店数据 + 指令
])
问题清单:
只返回对比分析文字,不超过100字是输出约束,应该在 SystemMessage用一段话对比分析是格式要求,应该在 SystemMessage- 只有酒店数据和预算才是变量数据
2.3 美食推荐 Agent
当前调用:
resp = invoke_with_retry(food_agent, [
SystemMessage(content=FOOD_SYSTEM_PROMPT), # 1句话
HumanMessage(content=prompt) # 18行
])
问题清单:
- HumanMessage 第一句
你是{dest}本地美食达人与 SystemMessage 重复,且混入了变量{dest} - 5 条要求中,
必须是真实存在的知名餐厅、给出招牌菜和人均消费、包含不同价位是固定规则 每天推荐午餐和晚餐各一家部分依赖 days(变量)- 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 验证方法
每次改动后验证:
- JSON 解析成功率(理想 >95%)
- 输出内容质量(人工抽查 3-5 个不同目的地)
- 不需要代码层面的兜底逻辑变更
八、核心要点
- SystemMessage 是"法律",HumanMessage 是"请求"——把规则和格式约束放在 SystemMessage,LLM 更倾向于遵守
- 每个子 Agent 的 SystemPrompt 要包含完整的角色、规则、格式、约束——而不是一行简介
- HumanMessage 只传变量数据——目的地、天数、POI 列表、预算等任务实例参数
- 子 Agent 对话不保存到 checkpoint——它们是无状态纯函数,这是正确的设计决策
- SystemMessage 中的固定内容可被 prompt cache 命中——这是分离固定/变量的实际收益
导航
← 上一篇:从 Checkpoint 到 State Sync | → 下一篇:多轮对话纠错