gogo-agent

通过问题改写融合多轮对话

用户:我想申请下周去杭州出差。 助手:好的,请告诉我具体出发和返回日期。 用户:周一去,周三回。 用户:帮我查一下那边的酒店 ← "那边"指哪?下游Agent看不懂

TL;DR

用户:我想申请下周去杭州出差。 助手:好的,请告诉我具体出发和返回日期。 用户:周一去,周三回。 用户:帮我查一下那边的酒店 ← "那边"指哪?下游Agent看不懂

在多轮对话中,用户的后续提问往往依赖前文上下文:

用户:我想申请下周去杭州出差。
助手:好的,请告诉我具体出发和返回日期。
用户:周一去,周三回。
用户:帮我查一下那边的酒店      ← "那边"指哪?下游Agent看不懂

如果直接把「帮我查一下那边的酒店」喂给意图识别或执行Agent,会遇到: - 意图识别无法判断这是杭州的酒店查询 - 执行Agent不知道目标城市和入住日期 解决方案:在下游处理之前,通过一次问题改写将多轮上下文「压缩」到一句自包含的问题中。 而且改写的话,还有一个好处,那就是有的时候问题太抽象,意图识别会错,经过改写之后,意图识别的准确率也能大大提升。后面我们介绍意图识别的时候会讲到,意图识别做了三层识别,越前面的成本月底,问题改写也能把本来需要L3识别的情况提前到L1就能识别(L1-L3是啥后面会讲),减少响应时长。 | 文件 | 职责 | | --- | --- | | QueryRewritingAgent.java | 继承 AgentBase,单次 LLM 调用实现改写,通过 Hook 获得多轮记忆 | | query-rewriting-agent-system.md | System Prompt:定义改写规则(指代消除、错别字、step-back 等) |

代码实现详解

QueryRewritingAgent — 继承 AgentBase 的轻量实现

@Component
@Scope
public


        messages


            messages


                        stableModel


}
  • 继承 AgentBase 而非 ReActAgent:问题改写是确定性的单步文本变换,无需工具调用和多轮推理循环,直接通过 model.stream() 发起单次 LLM 请求
  • @Scope("prototype"):每次请求新建实例,内存为空白状态
  • Hook 通过 @Autowired 注入:SessionPersistenceHook 在 Agent 执行前自动从 Session 加载历史对话。(后面会讲)
  • doCall() 中手动拼装 System Prompt:由于不使用 ReActAgent 的内置 sysPrompt 机制,在 doCall() 方法中显式将 prompt 作为第一条 SYSTEM 消息加入

System Prompt 的改写策略

query-rewriting-agent-system.md 定义了几个改写规则:

## 改写规则

### 1. 相关性判断

判断当前问题是否与历史对话相关:

- **相关**:当前问题是对前文话题的延续、追问、补充、确认或修正。
- **不相关**:当前问题开启了一个全新的、与历史无关的话题。

判断标准包括但不限于:

- 是否包含指代词(它、这个、那个、那边、上面、之前、上次等)。
- 是否省略了前文已提及的实体(城市、日期、酒店、航班、审批单号等)。
- 是否继续讨论同一出差任务、行程、审批或报销事项。

### 2. 问题融合与指代消除

当判断为相关时:

- 将当前问题中的省略和指代替换为历史中的具体实体。
- 把分散在多轮中的约束条件合并到当前问题中。
- 如果用户当前问题只是简单确认(如“确认”“好的”“就它吧”),则融合为完整动作描述,例如:“确认提交此前讨论的北京到杭州的差旅审批。”

### 3. 错别字改写

- 识别并修正用户输入中的常见错别字、拼音错误、形近字错误。
- 例如:`杭洲` → `杭州`、`差旅游` → `差旅`、`报消` → `报销`。
- 修正时必须保留原始语义,不得引入新信息。

### 4. Step-back 改写

- 当用户问题过于具体、模糊或带有强烈假设时,先退一步,生成一个更通用、更基础的问题版本,帮助理解真实意图。
- 例如:
  - 用户:`西湖附近那个全季怎么样?`
  - Step-back:`用户想了解杭州西湖附近全季酒店的相关信息(价格、位置、评价、是否符合差旅政策)。`
- Step-back 改写后的问题用于辅助理解,最终 `rewritten_question` 仍应保留具体细节。

### 5. 信息保留原则

改写后的问题**必须**满足:

- 不丢失原始问题中的关键实体(人、地点、时间、金额、审批单号、酒店/航班名等)。
- 不改变用户原始意图。
- 不添加历史中没有的假设信息,除非用于消除必要歧义。
- 对于不相关问题,原则上保持原问题不变,仅做错别字修正。

### 6. 面向下游的输出约束

- **禁止引入内部名称**:`rewritten_question` 会作为下游智能体的输入且可能在思考面板向用户展示,因此**绝不允许**向其中添加工具函数名、子智能体名、内部字段名与文件路径(如 `query_travel_order`、`plan_roundtrip`、`itineraryPlanAgent` 等 `snake_case` 或 camelCase 名称);`reason` 字段也应以中文自然语描述改写思路,不得引用此类英文标识符。

相关性判断

LLM 首先判断当前问题与历史是否相关: - 相关:含指代词(它/那边/上次)、省略了前文实体、延续同一任务 - 不相关:开启全新话题

指代消除与信息融合

历史:用户想去杭州出差,周一去周三回
当前:"帮我查一下那边的酒店"
改写:"帮我查一下杭州的酒店,入住日期为下周一,离店日期为下周三。"

将「那边」替换为「杭州」,并融合分散在多轮中的日期约束。

错别字修正

"我要报消一张发票" → "我要报销一张发票"

Step-back 改写

当问题过于具体或模糊时,先退一步理解真实意图:

"西湖附近那个全季怎么样?"
→ 用户想了解杭州西湖附近全季酒店的相关信息(价格、位置、评价、是否符合差旅政策)

输出约束

  • 不丢失原始关键实体
  • 不改变用户原始意图
  • 不添加历史中没有的假设信息
  • 不引入内部名称(tool 名、Agent 名等) 为了提升准确性,我们还使用了few-shot,增加了几个示例:
## 示例

### 示例 1:指代消除

History:

- 用户:我想申请下周去杭州出差。
- 助手:好的,请告诉我具体出发和返回日期。
- 用户:周一去,周三回。

Current question: `帮我查一下那边的酒店`

Output:

```json
{
  "related": true,
  "rewritten_question": "帮我查一下杭州的酒店,入住日期为下周一,离店日期为下周三。",
  "reason": "消除指代'那边',融合历史中的目的地和日期信息。"
}

示例 2:错别字修正

Current question: 我要报消一张发票

Output:

{
  "related": false,
  "rewritten_question": "我要报销一张发票",
  "reason": "修正错别字'报消'为'报销',当前问题未引用历史上下文。"
}

示例 3:不相关问题

History:

  • 用户:帮我查一下杭州的签证政策。
  • 助手:杭州属于中国境内,无需签证。

Current question: 今天北京天气怎么样?

Output:

{
  "related": false,
  "rewritten_question": "今天北京天气怎么样?",
  "reason": "当前问题与历史话题无关,保持原问题。"
}
## 一个完整的多轮示例

第1轮:用户 → "我想申请下周去杭州出差" L1命中 → 跳过改写 → 直接调度 ItineraryManageAgent 助手 → "好的,请告诉我出发和返回日期..."

第2轮:用户 → "周一去,周三回" L1命中(continuation信号) → 续跑 ItineraryManageAgent 助手 → "已创建差旅申请:杭州,7/21-7/23"

第3轮:用户 → "帮我查一下那边的酒店" L1/L2未命中 → 触发改写 QueryRewritingAgent 看到历史 → 输出: "帮我查一下杭州的酒店,入住7月21日,离店7月23日" 改写结果 → 意图识别命中 "行程规划" → 直跳 ItineraryPlanAgent 助手 → "已为您查询杭州7/21-7/23的酒店..."

第4轮:用户 → "第二个看起来不错,帮我订了" L1命中(continuation) → 续跑 ItineraryPlanAgent 助手 → "已为您预订xxx酒店"

```

版本提示

模型、框架与接口会持续变化。涉及版本号、参数与生产配置时,请在实践前对照对应官方文档。

LLMentor系统化学习大模型应用工程

内容来自个人课程知识库备份,并经过结构化整理。技术版本持续演进,生产使用前请结合官方文档验证。