question-routing-step-by-step.md 21 KB

用户提问后,问题会被传去哪儿

这份文档不再按“模块设计”讲,而是按你最关心的方式讲:

  • 用户问了一个问题
  • 这个问题先传到哪里
  • 那一层返回什么
  • 下一步再传到哪里

当前项目里,主要有两条链路:

  1. shudao-chat-py 的聊天链路
  2. shudao-aichat 的结构化意图识别 / 报告链路

1. 先记住最重要的区别

shudao-chat-py

更像:

  • 用户问题进来
  • 先做一个简单意图识别
  • 判断是不是问候 / FAQ / 知识库问题
  • 如果是知识库问题,就去查 RAG
  • 再让主模型生成最终回答

shudao-aichat

更像:

  • 用户问题进来
  • 先做一次“结构化意图识别”
  • 返回的不只是分类,而是一整套控制参数
  • 上层流程再根据这些参数决定下一步走在线、离线、内部查询还是直接结束

2. shudao-chat-py:用户问题一步步会去哪儿

这里先讲最容易理解的一条:shudao-chat-py 的问答链。


2.1 非流式问答链路

入口代码:

  • send_deepseek_message

第 1 步:前端把问题传进后端

用户在前端输入一个问题,比如:

高处作业安全防护有哪些要求?

这个问题会被传到:

  • send_deepseek_message

这一步拿到的内容是:

  • message,也就是用户原始问题
  • business_type,决定这是 AI 问答、PPT、大纲还是其他业务

如果 business_type == 0,表示走普通 AI 问答链路。

下一步:

  • message 传给意图识别函数 QwenService.intent_recognition

第 2 步:问题被传给意图识别 Prompt

代码位置:

  • QwenService.intent_recognition
  • Prompt 配置 prompt_config.yaml
  • Prompt 模板 yitushibie_template_lite.md

这一层会做的事是:

  1. 读取意图识别 Prompt 模板
  2. 把用户问题塞进模板里的 {userMessage}
  3. 形成一段新的提示词

传进去的是:

  • 用户原始问题

返回的不是最终答案,而是:

  • 一段给“意图识别模型”看的 Prompt

下一步:

  • 把这段 Prompt 传给 QwenService.chat

第 3 步:Prompt 被传给意图识别模型

代码位置:

  • QwenService.chat
  • 意图模型配置 qwen_service.py

这一层会:

  1. 组装 OpenAI 风格请求体
  2. 指定 model=self.intent_model
  3. 指定 api_url=self.intent_api_url
  4. 发 HTTP 请求到意图模型服务

传出去的是:

  • messages=[{"role":"user","content":"意图识别 Prompt"}]

返回回来的是:

  • 模型生成的一段文本
  • 理想情况下是一段 JSON 字符串

下一步:

  • 回到 QwenService.intent_recognition 解析这个模型输出

第 4 步:解析意图识别结果

代码位置:

  • qwen_service.py

这一层会做:

  1. 去掉可能存在的 Markdown 代码块
  2. 用正则提取 JSON
  3. json.loads() 解析
  4. 兼容 intentintent_type
  5. 统一得到 intent_type

这里希望返回的是类似这样的结果:

{
  "intent_type": "query_knowledge_base",
  "confidence": 0.9,
  "search_queries": ["高处作业安全防护要求"],
  "response": ""
}

或者:

{
  "intent_type": "greeting",
  "confidence": 0.95,
  "search_queries": [],
  "response": "您好!我是蜀安AI助手,很高兴为您服务。"
}

如果解析失败,返回的是:

{
  "intent_type": "general_chat",
  "confidence": 0.5,
  "reason": "无法解析JSON",
  "response": ""
}

下一步:

  • 路由层根据 intent_type 判断接下来去哪儿

第 5 步:根据 intent_type 判断下一步去哪儿

代码位置:

  • send_deepseek_message
  • 直接回复逻辑 qwen_service.py

这里分三种典型情况。

情况 A:greeting(问候)

比如:

  • 你好
  • 在吗
  • 谢谢

返回的内容通常是:

  • 一段固定欢迎语 response

下一步:

  • 不查知识库
  • 直接把 response 作为结果返回给前端

情况 B:faq(常见问题)

比如:

  • 你是谁
  • 你能做什么

返回的内容通常是:

  • 一段关于 AI 助手自己的介绍

下一步:

  • 不查知识库
  • 直接返回给前端

情况 C:query_knowledge_base(知识库查询)

比如:

  • 高处作业安全防护有哪些要求
  • 隧道施工通风规范是什么

返回的是:

  • 一个“需要查知识库”的判定
  • response 一般为空

下一步:

  • 把用户原问题传给 RAG 检索函数 _rag_search

第 6 步:问题被传给 RAG 检索服务

代码位置:

  • _rag_search

这一层会做:

  1. 读取搜索服务地址
  2. 发 HTTP 请求给检索服务
  3. 请求体里带:
    • query=用户原问题
    • n_results=top_k
  4. 从返回结果里提取文档内容
  5. 拼成一大段 rag_context

传出去的是:

  • 用户原问题

返回回来的是:

  • 检索到的文档内容拼接文本 rag_context

下一步:

  • rag_context 和用户问题一起传给最终回答 Prompt

第 7 步:用户问题 + 检索结果被传给最终回答 Prompt

代码位置:

  • Prompt 配置 prompt_config.yaml
  • Prompt 模板 final_answer_template.md
  • 路由调用点 chat.py

这一层会把这些内容组合起来:

  • 用户原问题 message
  • 检索结果 rag_context

返回的不是最终回答,而是:

  • 一段“让主模型生成最终答案”的 Prompt

下一步:

  • 把这段 Prompt 传给主模型调用 QwenService.chat

第 8 步:主模型生成最终答案

代码位置:

  • QwenService.chat
  • 路由调用点 chat.py

这一层传出去的是:

  • 最终回答 Prompt

返回回来的是:

  • 主模型生成的一整段回答文本

如果模型输出里包含 <think>...</think>,说明它把思考过程和正式回答一起吐出来了。

下一步:

  • 先拆分思考过程和正式回答

第 9 步:如果有 <think>,先拆分再二次总结

代码位置:

  • 拆分函数 split_thinking_and_answer
  • 摘要主函数 thinking_summary.py
  • 路由调用点 chat.py

这一层会:

  1. 提取 <think> 里的原始思考内容
  2. 把正式回答分离出来
  3. 再调用一个“思考摘要”逻辑
  4. 生成可展示的中文摘要

返回回来的是:

  • thinking_summary
  • answer_text

下一步:

  • 把两者拼成最终展示结果返回给前端

第 10 步:把最终结果返回给前端

代码位置:

  • send_deepseek_message

最终返回给前端的一般是:

  • response
  • reply
  • content
  • message

这些字段本质上都在装“同一份最终回答”。


2.2 无 DB 流式问答链路

入口代码:

  • stream_chat

这条链和上面很像,只是“最后返回”不是一次性返回整段文本,而是边生成边推送。

第 1 步到第 5 步

和上面的非流式问答基本一样:

  1. 用户问题进入 stream_chat
  2. 问题传给 QwenService.intent_recognition
  3. 返回 intent_type
  4. 如果是知识库问题,传给 _rag_search
  5. 组合最终回答 Prompt

第 6 步:传给流式主模型

代码位置:

  • QwenService.stream_chat
  • 路由调用点 chat.py

传出去的是:

  • 最终回答 Prompt

返回回来的是:

  • 一个个文本分块 chunk

下一步:

  • 后端边收到 chunk,边通过 SSE 推给前端

第 7 步:如果遇到 <think>,先拦住做摘要

代码位置:

  • chat.py

这一层会:

  1. 边读模型流式输出
  2. 一旦发现 <think>
  3. 先把思考内容收集起来
  4. 调用摘要逻辑
  5. 给前端发“思考过程摘要”
  6. 再继续发正式回答

返回给前端的是:

  • 一段段 SSE 消息

最后一步:

  • 发送 [DONE] 结束流

2.3 带 DB 的主聊天链路

入口代码:

  • stream_chat_with_db

这条最重要,但也是最容易误解的一条。

关键结论

这条链路里:

  • 用户问题不会先传给意图识别
  • 而是直接进入“建会话 -> 写消息 -> RAG -> 主模型流式回答”

第 1 步:用户问题进入主聊天接口

代码位置:

  • stream_chat_with_db

传进来的是:

  • message
  • ai_conversation_id
  • business_type
  • online_search_content

下一步:

  • 创建或复用会话

第 2 步:创建 / 获取对话

代码位置:

  • chat.py

返回的是:

  • conv_id

下一步:

  • 把用户消息写入数据库

第 3 步:写入 user 消息,再写一个 ai 占位消息

代码位置:

  • 写 user 消息 chat.py
  • 写 ai 占位消息 chat.py

返回的是:

  • user_msg.id
  • ai_msg.id

下一步:

  • 先给前端发一个 initial 事件

第 4 步:先把会话 ID 和消息 ID 回给前端

代码位置:

  • chat.py

返回的是:

  • ai_conversation_id
  • ai_message_id

下一步:

  • 直接做 RAG 检索

第 5 步:用户问题直接传给 RAG

代码位置:

  • 调用点 chat.py
  • RAG 函数 _rag_search

传出去的是:

  • 用户原问题 message

返回的是:

  • rag_context

下一步:

  • 构建历史上下文 + 最终回答 Prompt

第 6 步:读取最近历史消息,拼历史上下文

代码位置:

  • chat.py

返回的是:

  • history_context

下一步:

  • message + rag_context + history_context (+ online_search_content) 一起传给最终回答 Prompt

第 7 步:把完整上下文传给主模型

代码位置:

  • Prompt 组装 chat.py
  • 流式主模型调用 chat.py

传给主模型的是:

  • 用户问题
  • RAG 文本
  • 在线搜索文本
  • 最近几轮历史消息

返回的是:

  • 流式文本块

下一步:

  • 边收到边推送给前端,同时累积完整回答

第 8 步:如果有 <think>,先做摘要再继续回答

代码位置:

  • chat.py

返回给前端的是:

  • 先一段“思考过程摘要”
  • 再一段段正式回答

下一步:

  • 流结束后把完整回答写回数据库

第 9 步:把完整 AI 回答写回数据库

代码位置:

  • chat.py

写回的是:

  • AIMessage.content = full_response

最后一步:

  • 发送 [DONE]

代码位置:

  • chat.py

3. shudao-aichat:用户问题一步步会去哪儿

这条链不是普通聊天链,而是“结构化意图识别 + 报告主流程”的链。


3.1 独立意图识别链

入口代码:

  • analyze_intent

第 1 步:用户问题进入意图识别接口

传进来的字段定义在:

  • IntentAnalyzeRequest

包括:

  • user_question
  • conversation_history
  • enable_online_model

下一步:

  • 传给意图识别 Prompt 构造器

第 2 步:问题被传给结构化 Prompt

代码位置:

  • get_intent_prompt
  • 模板 intent_analysis_prompt.md

这一层会把这些信息塞进 Prompt:

  • 用户问题
  • 对话历史
  • 是否启用在线模型

返回的是:

  • 一段结构化意图识别 Prompt

下一步:

  • 再加上 system 规则和 JSON schema

第 3 步:给模型加 system 规则和固定输出格式

代码位置:

  • system 规则 intent.py
  • schema 构造 _build_intent_response_format

这一层不是返回答案,而是给模型增加限制:

  • 什么算非专业问题
  • 不允许输出原始推理链
  • 必须返回 JSON
  • JSON 要符合固定字段结构

下一步:

  • 把整套消息传给离线 LLM

第 4 步:把问题传给离线意图模型

代码位置:

  • 调用点 intent.py
  • 服务实现 OfflineLLMService.chat

传出去的是:

  • system 消息
  • user Prompt
  • schema 约束

返回的是:

  • 模型原始文本

理想情况下,这个文本应该是一个合法 JSON。

下一步:

  • 解析 JSON

第 5 步:如果 JSON 不合格,先重试一次

代码位置:

  • 重试提示词 _build_retry_messages
  • 重试逻辑 intent.py

如果第一次返回的不是合法 JSON:

  • 系统会再构造一组更严格的消息
  • 再请求一次模型

返回的仍然是:

  • 模型文本

下一步:

  • 进入统一 JSON 解析器

第 6 步:把模型输出解析成结构化结果

代码位置:

  • parse_ai_json_response
  • 调用点 intent.py

这一层返回的是一个结构化字典,可能包含:

  • is_professional_question
  • route
  • need_offline_model
  • origin_question
  • keywords
  • fallback_keywords
  • intent_scene
  • company_name
  • company_aliases
  • summary
  • reasoning_summary

下一步:

  • 生成给前端看的 thinking_content

第 7 步:把 reasoning_summary 加工成前端可展示摘要

代码位置:

  • intent.py

这一层返回的是:

  • thinking_content

它不是原始推理链,而是安全可展示的摘要。

下一步:

  • 如果前面失败太多,就走本地兜底;否则直接组装响应

第 8 步:如果模型不可靠,就走本地兜底

代码位置:

  • 兜底函数 _build_fallback_intent_result
  • 触发点 intent.py

返回的是:

  • 一份简化版意图识别结果

比如:

  • 是否专业问题
  • 走在线还是离线
  • 默认关键词

下一步:

  • 统一组装接口响应

第 9 步:返回标准化的意图识别结果

代码位置:

  • intent.py
  • 响应模型 IntentAnalyzeResponse

最终返回给上层的是:

  • is_professional_question
  • route
  • keywords
  • fallback_keywords
  • intent_scene
  • company_name
  • company_aliases
  • summary
  • thinking_content

下一步:

  • 交给报告主流程继续判断往哪走

3.2 报告主流程怎么接这份意图结果

代码位置:

  • report.py

第 1 步:报告主流程先调用意图识别

调用点:

  • report.py

传进去的是:

  • 用户问题
  • 对话历史
  • 在线模型开关

返回的是:

  • IntentAnalyzeResponse

下一步:

  • 根据 routeis_professional_question 分支

第 2 步:如果是 online_only(仅在线)

代码位置:

  • report.py

下一步:

  • 直接去在线回答流程

第 3 步:如果是 online_then_offline(先在线后离线)

代码位置:

  • report.py

下一步:

  • 一边走在线回答
  • 一边继续离线检索 / 报告生成

第 4 步:如果不是专业问题

代码位置:

  • report.py

下一步:

  • 直接停止后面的专业检索流程

第 5 步:如果是 internal_query(内部查询)

代码位置:

  • report.py

下一步:

  • 走内部文档检索分支

4. 你最该记住的“问题流向”

如果只保留最简版,可以这样记:

shudao-chat-py

用户问题
  -> 聊天路由
  -> 意图识别小模型
  -> 返回 intent_type
  -> 如果需要查库,就去 RAG
  -> 把问题 + RAG结果 传给主模型
  -> 主模型生成最终回答
  -> 如果有 think,再做思考摘要
  -> 返回前端

shudao-chat-py 主聊天接口 stream/chat-with-db

用户问题
  -> 创建/复用会话
  -> 写 user 消息
  -> 写 ai 占位消息
  -> 直接去 RAG
  -> 拼历史上下文
  -> 传给主模型流式生成
  -> 边输出边写回数据库

shudao-aichat

用户问题
  -> 结构化意图识别
  -> 返回 route / keywords / scene / summary
  -> 报告主流程读取这些字段
  -> 决定下一步走在线、离线、内部查询还是直接结束

5. 最后一句话

如果你问的是:

  • “用户的问题最后是怎么变成回答的?”

那最直接的答案就是:

shudao-chat-py

  • 用户问题先可能去“意图识别”
  • 再可能去“RAG 检索”
  • 然后一定会去“主模型生成最终回答”

shudao-aichat

  • 用户问题先去“结构化意图识别”
  • 这个步骤先不直接给最终答案
  • 它先返回“下一步该怎么走”
  • 然后上层流程再继续往下跑