✅基于规则+RAG+LLM构建三层意图识别
意图识别嘛,把用户的话丢给大模型,让它输出一个分类 JSON 不就行了?能跑,但是实际企业里面用起来,这个"纯 LLM"方案有三个致命成本: gogo-agent 的解法是把意图识别做成一条短路流水线:先用最便宜、最快、最确定的手段试,试 LLMentor 前面我们讲了三层意图识别,他们不是孤立存在的,它嵌在 AgentPipelineService 这条主流水线里。这里有两个精彩的工程优化。
先用原始问题快筛,未命中再改写
public Mono<Msg> executeFullPipeline(List<Msg> inputMessages, String sessionId, String userId) {
String originalQuestion = extractLatestUserText(inputMessages);
// 阶段一:先用【原始问题】做 L1/L2 快速识别(router.route 不触发 L3)
return Mono.fromCallable(() -> intentRecognitionRouter.route(originalQuestion))
.subscribeOn(Schedulers.boundedElastic())
.flatMap(fastHit -> {
if (fastHit.isPresent()) {
// 命中:跳过问题改写,直接调度
String intentJson = JSON.toJSONString(fastHit.get().toJsonMap());
logger.info("[PIPELINE] L1/L2 命中,跳过问题改写,直接调度: {}", intentJson);
// 快路径没真正调用 QueryRewritingAgent,补一条"影子历史"写回 Session,
// 以免下一轮未命中 L1/L2 时改写 Agent 看不到本轮上下文
appendQueryRewritingShadowHistory(sessionId, originalQuestion);
return dispatchByIntent(intentJson, inputMessages, originalQuestion, sessionId, userId);
}
// 未命中:阶段二——先问题改写,再走完整 L1/L2/L3
return rewriteThenRecognizeAndDispatch(inputMessages, sessionId, userId);
});
}
/**
* L1/L2 未命中时的降级流程:问题改写(QueryRewritingAgent)→ 重新意图识别
* (IntentRecognitionAgent,此时走完整 L1/L2/L3)→ 调度。
*/
private Mono<Msg> rewriteThenRecognizeAndDispatch(List<Msg> inputMessages, String sessionId, String userId) {
QueryRewritingAgent queryRewritingAgent = agentRegistry.getQueryRewriter();
IntentRecognitionAgent intentRecognitionAgent = agentRegistry.getIntentRecognizer();
return queryRewritingAgent.call(inputMessages)
.doOnSubscribe(s -> executionRegistry.register(sessionId, queryRewritingAgent))
.flatMap(rewriteResult -> {
if (isInterruptRecovery(rewriteResult)) {
logger.info("[PIPELINE] QueryRewritingAgent 被优雅中断,终止流水线并返回中文提示");
return Mono.just(buildInterruptRecoveryMsg());
}
String rewrittenQuestion = parseRewrittenQuestion(rewriteResult.getTextContent());
logger.info("[PIPELINE] 问题改写完成: {}", rewrittenQuestion);
return intentRecognitionAgent.call(buildIntentInput(rewrittenQuestion))
.doOnSubscribe(s -> executionRegistry.register(sessionId, intentRecognitionAgent))
.flatMap(intentResult -> {
if (isInterruptRecovery(intentResult)) {
logger.info("[PIPELINE] IntentRecognitionAgent 被优雅中断,终止流水线并返回中文提示");
return Mono.just(buildInterruptRecoveryMsg());
}
String intentJson = intentResult.getTextContent();
logger.info("[PIPELINE] 意图识别完成: {}", intentJson);
conversationTitleService.updateTitleAsync(sessionId, userId, rewrittenQuestion, intentJson);
return dispatchByIntent(intentJson, inputMessages, rewrittenQuestion, sessionId, userId);
});
});
}
这个"两阶段"设计的巧思在于:问题改写(消除指代、补全省略)本身也要调一次大模型,不便宜。 所以流水线先拿原始问题去撞 L1/L2——如果一句"帮我报销"直接命中 L1,那连改写都省了,直接调度。 如果没有命中L1或者L2,那说明用户的问题问的非常不标准,那么我们需要先做一次改写,靠LLM来给用户的问题做一次规范化。规范化之后,我们再走一遍L1\L2\L3的完整流程。这时候因为问题被改写过,所有有很大概率可以命中L1或者L2。 最差的情况就是改写了,L1/L2还是没命中,再走了L3了。。。。。。。 代价是要处理一个边界:快路径没真调 QueryRewritingAgent,它的 Session 历史会是空的,下一轮若走改写就没上下文。解法是 appendQueryRewritingShadowHistory——合成一条"影子历史"(related=false、改写结果=原问题)写回 Session,让改写 Agent 下轮仍能看到本轮上下文。这是"快路径优化"必须补的一致性欠账。
✅非ReActAgent如何做Session持久化
无状态 Agent,比如我们的QueryRewritingAgent、IntentRecognitionAgent 这类"单次调用、不持有 Memory"的分析型 Agent。我们也需要针对他们做session的持久化,但是这类Agent他 LLMentor
典型case
Case A:用户发"你好" 阶段一用原始问题撞 L1 → GREETING 规则整句命中 → 高置信单意图 → JSON 里 target_agent=masterAgent(不在直跳白名单)→ 走 MasterAgent 寒暄。全程 0 次大模型调用,延迟 < 50ms。 Case B:用户发"这趟出差垫付的钱帮我弄回来" L1 无任何关键词命中(没有"报销/发票"字样)→ 下沉 L2 → 向量检索命中 reimbursement 语料"这趟出差垫付的钱,帮我弄回来",相似度 0.9 → 高置信。走报销agent。因为只做了 1 次 embedding,没调分类大模型,延迟 ~100ms。 Case C:用户发"审批过了,帮我规划下杭州行程再订个酒店" L1 未命中("审批过了"是状态陈述、又混了规划+订酒店)→ L2 单条最近邻也判不清多意图 → 返回 empty → 流水线阶段二:先 QueryRewritingAgent 改写(消除"审批过了"的指代、补全)→ IntentRecognitionAgent 走 L3 LLM → 输出多意图 [itinerary_planning, hotel_search](multi_intent=true)→ 因多意图不满足直跳 → 走 MasterAgent 按依赖顺序编排"先规划、后订酒店"。这才是真正需要大模型的场景。