gogo-agent

意图识别与问题改写的流水线编排

✅基于规则+RAG+LLM构建三层意图识别 意图识别嘛,把用户的话丢给大模型,让它输出一个分类 JSON 不就行了?能跑,但是实际企业里面用起来,这个"纯 LLM"方案有三个致命成本: gogo agent 的解法是把意图识别做成一…

TL;DR

✅基于规则+RAG+LLM构建三层意图识别 意图识别嘛,把用户的话丢给大模型,让它输出一个分类 JSON 不就行了?能跑,但是实际企业里面用起来,这个"纯 LLM"方案有三个致命成本: gogo agent 的解法是把意图识别做成一…

✅基于规则+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 按依赖顺序编排"先规划、后订酒店"。这才是真正需要大模型的场景。

版本提示

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

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

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