# 用户提问后,问题会被传去哪儿 这份文档不再按“模块设计”讲,而是按你最关心的方式讲: - 用户问了一个问题 - 这个问题先传到哪里 - 那一层返回什么 - 下一步再传到哪里 当前项目里,主要有两条链路: 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](file:///Users/fanhong/UGIT/shudao-main/shudao-chat-py/routers/chat.py#L893-L1183) ### 第 1 步:前端把问题传进后端 用户在前端输入一个问题,比如: ```text 高处作业安全防护有哪些要求? ``` 这个问题会被传到: - [send_deepseek_message](file:///Users/fanhong/UGIT/shudao-main/shudao-chat-py/routers/chat.py#L893-L1183) 这一步拿到的内容是: - `message`,也就是用户原始问题 - `business_type`,决定这是 AI 问答、PPT、大纲还是其他业务 如果 `business_type == 0`,表示走普通 AI 问答链路。 下一步: - 把 `message` 传给意图识别函数 [QwenService.intent_recognition](file:///Users/fanhong/UGIT/shudao-main/shudao-chat-py/services/qwen_service.py#L73-L116) ### 第 2 步:问题被传给意图识别 Prompt 代码位置: - [QwenService.intent_recognition](file:///Users/fanhong/UGIT/shudao-main/shudao-chat-py/services/qwen_service.py#L73-L116) - Prompt 配置 [prompt_config.yaml](file:///Users/fanhong/UGIT/shudao-main/shudao-chat-py/config/prompt_config.yaml#L4-L10) - Prompt 模板 [yitushibie_template_lite.md](file:///Users/fanhong/UGIT/shudao-main/shudao-chat-py/prompts/yitushibie_template_lite.md#L1-L46) 这一层会做的事是: 1. 读取意图识别 Prompt 模板 2. 把用户问题塞进模板里的 `{userMessage}` 3. 形成一段新的提示词 传进去的是: - 用户原始问题 返回的不是最终答案,而是: - 一段给“意图识别模型”看的 Prompt 下一步: - 把这段 Prompt 传给 [QwenService.chat](file:///Users/fanhong/UGIT/shudao-main/shudao-chat-py/services/qwen_service.py#L117-L207) ### 第 3 步:Prompt 被传给意图识别模型 代码位置: - [QwenService.chat](file:///Users/fanhong/UGIT/shudao-main/shudao-chat-py/services/qwen_service.py#L117-L207) - 意图模型配置 [qwen_service.py](file:///Users/fanhong/UGIT/shudao-main/shudao-chat-py/services/qwen_service.py#L14-L29) 这一层会: 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](file:///Users/fanhong/UGIT/shudao-main/shudao-chat-py/services/qwen_service.py#L73-L116) 解析这个模型输出 ### 第 4 步:解析意图识别结果 代码位置: - [qwen_service.py](file:///Users/fanhong/UGIT/shudao-main/shudao-chat-py/services/qwen_service.py#L85-L115) 这一层会做: 1. 去掉可能存在的 Markdown 代码块 2. 用正则提取 JSON 3. `json.loads()` 解析 4. 兼容 `intent` 和 `intent_type` 5. 统一得到 `intent_type` 这里希望返回的是类似这样的结果: ```json { "intent_type": "query_knowledge_base", "confidence": 0.9, "search_queries": ["高处作业安全防护要求"], "response": "" } ``` 或者: ```json { "intent_type": "greeting", "confidence": 0.95, "search_queries": [], "response": "您好!我是蜀安AI助手,很高兴为您服务。" } ``` 如果解析失败,返回的是: ```json { "intent_type": "general_chat", "confidence": 0.5, "reason": "无法解析JSON", "response": "" } ``` 下一步: - 路由层根据 `intent_type` 判断接下来去哪儿 ### 第 5 步:根据 `intent_type` 判断下一步去哪儿 代码位置: - [send_deepseek_message](file:///Users/fanhong/UGIT/shudao-main/shudao-chat-py/routers/chat.py#L959-L1016) - 直接回复逻辑 [qwen_service.py](file:///Users/fanhong/UGIT/shudao-main/shudao-chat-py/services/qwen_service.py#L100-L109) 这里分三种典型情况。 #### 情况 A:`greeting(问候)` 比如: - 你好 - 在吗 - 谢谢 返回的内容通常是: - 一段固定欢迎语 `response` 下一步: - 不查知识库 - 直接把 `response` 作为结果返回给前端 #### 情况 B:`faq(常见问题)` 比如: - 你是谁 - 你能做什么 返回的内容通常是: - 一段关于 AI 助手自己的介绍 下一步: - 不查知识库 - 直接返回给前端 #### 情况 C:`query_knowledge_base(知识库查询)` 比如: - 高处作业安全防护有哪些要求 - 隧道施工通风规范是什么 返回的是: - 一个“需要查知识库”的判定 - `response` 一般为空 下一步: - 把用户原问题传给 RAG 检索函数 [_rag_search](file:///Users/fanhong/UGIT/shudao-main/shudao-chat-py/routers/chat.py#L529-L553) ### 第 6 步:问题被传给 RAG 检索服务 代码位置: - [_rag_search](file:///Users/fanhong/UGIT/shudao-main/shudao-chat-py/routers/chat.py#L529-L553) 这一层会做: 1. 读取搜索服务地址 2. 发 HTTP 请求给检索服务 3. 请求体里带: - `query=用户原问题` - `n_results=top_k` 4. 从返回结果里提取文档内容 5. 拼成一大段 `rag_context` 传出去的是: - 用户原问题 返回回来的是: - 检索到的文档内容拼接文本 `rag_context` 下一步: - 把 `rag_context` 和用户问题一起传给最终回答 Prompt ### 第 7 步:用户问题 + 检索结果被传给最终回答 Prompt 代码位置: - Prompt 配置 [prompt_config.yaml](file:///Users/fanhong/UGIT/shudao-main/shudao-chat-py/config/prompt_config.yaml#L18-L29) - Prompt 模板 [final_answer_template.md](file:///Users/fanhong/UGIT/shudao-main/shudao-chat-py/prompts/final_answer_template.md) - 路由调用点 [chat.py](file:///Users/fanhong/UGIT/shudao-main/shudao-chat-py/routers/chat.py#L974-L985) 这一层会把这些内容组合起来: - 用户原问题 `message` - 检索结果 `rag_context` 返回的不是最终回答,而是: - 一段“让主模型生成最终答案”的 Prompt 下一步: - 把这段 Prompt 传给主模型调用 [QwenService.chat](file:///Users/fanhong/UGIT/shudao-main/shudao-chat-py/services/qwen_service.py#L117-L207) ### 第 8 步:主模型生成最终答案 代码位置: - [QwenService.chat](file:///Users/fanhong/UGIT/shudao-main/shudao-chat-py/services/qwen_service.py#L117-L207) - 路由调用点 [chat.py](file:///Users/fanhong/UGIT/shudao-main/shudao-chat-py/routers/chat.py#L985-L1015) 这一层传出去的是: - 最终回答 Prompt 返回回来的是: - 主模型生成的一整段回答文本 如果模型输出里包含 `...`,说明它把思考过程和正式回答一起吐出来了。 下一步: - 先拆分思考过程和正式回答 ### 第 9 步:如果有 ``,先拆分再二次总结 代码位置: - 拆分函数 [split_thinking_and_answer](file:///Users/fanhong/UGIT/shudao-main/shudao-chat-py/utils/thinking_summary.py#L115-L168) - 摘要主函数 [thinking_summary.py](file:///Users/fanhong/UGIT/shudao-main/shudao-chat-py/utils/thinking_summary.py#L261-L380) - 路由调用点 [chat.py](file:///Users/fanhong/UGIT/shudao-main/shudao-chat-py/routers/chat.py#L1001-L1015) 这一层会: 1. 提取 `` 里的原始思考内容 2. 把正式回答分离出来 3. 再调用一个“思考摘要”逻辑 4. 生成可展示的中文摘要 返回回来的是: - `thinking_summary` - `answer_text` 下一步: - 把两者拼成最终展示结果返回给前端 ### 第 10 步:把最终结果返回给前端 代码位置: - [send_deepseek_message](file:///Users/fanhong/UGIT/shudao-main/shudao-chat-py/routers/chat.py#L1166-L1178) 最终返回给前端的一般是: - `response` - `reply` - `content` - `message` 这些字段本质上都在装“同一份最终回答”。 --- ## 2.2 无 DB 流式问答链路 入口代码: - [stream_chat](file:///Users/fanhong/UGIT/shudao-main/shudao-chat-py/routers/chat.py#L1364-L1484) 这条链和上面很像,只是“最后返回”不是一次性返回整段文本,而是边生成边推送。 ### 第 1 步到第 5 步 和上面的非流式问答基本一样: 1. 用户问题进入 [stream_chat](file:///Users/fanhong/UGIT/shudao-main/shudao-chat-py/routers/chat.py#L1364-L1484) 2. 问题传给 [QwenService.intent_recognition](file:///Users/fanhong/UGIT/shudao-main/shudao-chat-py/services/qwen_service.py#L73-L116) 3. 返回 `intent_type` 4. 如果是知识库问题,传给 [_rag_search](file:///Users/fanhong/UGIT/shudao-main/shudao-chat-py/routers/chat.py#L529-L553) 5. 组合最终回答 Prompt ### 第 6 步:传给流式主模型 代码位置: - [QwenService.stream_chat](file:///Users/fanhong/UGIT/shudao-main/shudao-chat-py/services/qwen_service.py#L208-L256) - 路由调用点 [chat.py](file:///Users/fanhong/UGIT/shudao-main/shudao-chat-py/routers/chat.py#L1407-L1482) 传出去的是: - 最终回答 Prompt 返回回来的是: - 一个个文本分块 `chunk` 下一步: - 后端边收到 `chunk`,边通过 SSE 推给前端 ### 第 7 步:如果遇到 ``,先拦住做摘要 代码位置: - [chat.py](file:///Users/fanhong/UGIT/shudao-main/shudao-chat-py/routers/chat.py#L1410-L1477) 这一层会: 1. 边读模型流式输出 2. 一旦发现 `` 3. 先把思考内容收集起来 4. 调用摘要逻辑 5. 给前端发“思考过程摘要” 6. 再继续发正式回答 返回给前端的是: - 一段段 SSE 消息 最后一步: - 发送 `[DONE]` 结束流 --- ## 2.3 带 DB 的主聊天链路 入口代码: - [stream_chat_with_db](file:///Users/fanhong/UGIT/shudao-main/shudao-chat-py/routers/chat.py#L1500-L1804) 这条最重要,但也是最容易误解的一条。 ### 关键结论 这条链路里: - 用户问题**不会先传给意图识别** - 而是直接进入“建会话 -> 写消息 -> RAG -> 主模型流式回答” ### 第 1 步:用户问题进入主聊天接口 代码位置: - [stream_chat_with_db](file:///Users/fanhong/UGIT/shudao-main/shudao-chat-py/routers/chat.py#L1500-L1521) 传进来的是: - `message` - `ai_conversation_id` - `business_type` - `online_search_content` 下一步: - 创建或复用会话 ### 第 2 步:创建 / 获取对话 代码位置: - [chat.py](file:///Users/fanhong/UGIT/shudao-main/shudao-chat-py/routers/chat.py#L1524-L1579) 返回的是: - `conv_id` 下一步: - 把用户消息写入数据库 ### 第 3 步:写入 user 消息,再写一个 ai 占位消息 代码位置: - 写 user 消息 [chat.py](file:///Users/fanhong/UGIT/shudao-main/shudao-chat-py/routers/chat.py#L1580-L1593) - 写 ai 占位消息 [chat.py](file:///Users/fanhong/UGIT/shudao-main/shudao-chat-py/routers/chat.py#L1594-L1607) 返回的是: - `user_msg.id` - `ai_msg.id` 下一步: - 先给前端发一个 initial 事件 ### 第 4 步:先把会话 ID 和消息 ID 回给前端 代码位置: - [chat.py](file:///Users/fanhong/UGIT/shudao-main/shudao-chat-py/routers/chat.py#L1609-L1610) 返回的是: - `ai_conversation_id` - `ai_message_id` 下一步: - 直接做 RAG 检索 ### 第 5 步:用户问题直接传给 RAG 代码位置: - 调用点 [chat.py](file:///Users/fanhong/UGIT/shudao-main/shudao-chat-py/routers/chat.py#L1612-L1613) - RAG 函数 [_rag_search](file:///Users/fanhong/UGIT/shudao-main/shudao-chat-py/routers/chat.py#L529-L553) 传出去的是: - 用户原问题 `message` 返回的是: - `rag_context` 下一步: - 构建历史上下文 + 最终回答 Prompt ### 第 6 步:读取最近历史消息,拼历史上下文 代码位置: - [chat.py](file:///Users/fanhong/UGIT/shudao-main/shudao-chat-py/routers/chat.py#L1628-L1667) 返回的是: - `history_context` 下一步: - 把 `message + rag_context + history_context (+ online_search_content)` 一起传给最终回答 Prompt ### 第 7 步:把完整上下文传给主模型 代码位置: - Prompt 组装 [chat.py](file:///Users/fanhong/UGIT/shudao-main/shudao-chat-py/routers/chat.py#L1648-L1667) - 流式主模型调用 [chat.py](file:///Users/fanhong/UGIT/shudao-main/shudao-chat-py/routers/chat.py#L1683-L1755) 传给主模型的是: - 用户问题 - RAG 文本 - 在线搜索文本 - 最近几轮历史消息 返回的是: - 流式文本块 下一步: - 边收到边推送给前端,同时累积完整回答 ### 第 8 步:如果有 ``,先做摘要再继续回答 代码位置: - [chat.py](file:///Users/fanhong/UGIT/shudao-main/shudao-chat-py/routers/chat.py#L1690-L1774) 返回给前端的是: - 先一段“思考过程摘要” - 再一段段正式回答 下一步: - 流结束后把完整回答写回数据库 ### 第 9 步:把完整 AI 回答写回数据库 代码位置: - [chat.py](file:///Users/fanhong/UGIT/shudao-main/shudao-chat-py/routers/chat.py#L1776-L1793) 写回的是: - `AIMessage.content = full_response` 最后一步: - 发送 `[DONE]` 代码位置: - [chat.py](file:///Users/fanhong/UGIT/shudao-main/shudao-chat-py/routers/chat.py#L1795-L1796) --- ## 3. `shudao-aichat`:用户问题一步步会去哪儿 这条链不是普通聊天链,而是“结构化意图识别 + 报告主流程”的链。 --- ## 3.1 独立意图识别链 入口代码: - [analyze_intent](file:///Users/fanhong/UGIT/shudao-aichat/app/api/intent.py#L237-L392) ### 第 1 步:用户问题进入意图识别接口 传进来的字段定义在: - [IntentAnalyzeRequest](file:///Users/fanhong/UGIT/shudao-aichat/app/schemas/models.py#L9-L13) 包括: - `user_question` - `conversation_history` - `enable_online_model` 下一步: - 传给意图识别 Prompt 构造器 ### 第 2 步:问题被传给结构化 Prompt 代码位置: - [get_intent_prompt](file:///Users/fanhong/UGIT/shudao-aichat/app/utils/prompts.py#L70-L80) - 模板 [intent_analysis_prompt.md](file:///Users/fanhong/UGIT/shudao-aichat/prompts/intent_analysis_prompt.md#L1-L236) 这一层会把这些信息塞进 Prompt: - 用户问题 - 对话历史 - 是否启用在线模型 返回的是: - 一段结构化意图识别 Prompt 下一步: - 再加上 system 规则和 JSON schema ### 第 3 步:给模型加 system 规则和固定输出格式 代码位置: - system 规则 [intent.py](file:///Users/fanhong/UGIT/shudao-aichat/app/api/intent.py#L258-L272) - schema 构造 [_build_intent_response_format](file:///Users/fanhong/UGIT/shudao-aichat/app/api/intent.py#L24-L77) 这一层不是返回答案,而是给模型增加限制: - 什么算非专业问题 - 不允许输出原始推理链 - 必须返回 JSON - JSON 要符合固定字段结构 下一步: - 把整套消息传给离线 LLM ### 第 4 步:把问题传给离线意图模型 代码位置: - 调用点 [intent.py](file:///Users/fanhong/UGIT/shudao-aichat/app/api/intent.py#L273-L296) - 服务实现 [OfflineLLMService.chat](file:///Users/fanhong/UGIT/shudao-aichat/app/services/offline_llm_service.py#L30-L103) 传出去的是: - system 消息 - user Prompt - schema 约束 返回的是: - 模型原始文本 理想情况下,这个文本应该是一个合法 JSON。 下一步: - 解析 JSON ### 第 5 步:如果 JSON 不合格,先重试一次 代码位置: - 重试提示词 [_build_retry_messages](file:///Users/fanhong/UGIT/shudao-aichat/app/api/intent.py#L79-L103) - 重试逻辑 [intent.py](file:///Users/fanhong/UGIT/shudao-aichat/app/api/intent.py#L281-L319) 如果第一次返回的不是合法 JSON: - 系统会再构造一组更严格的消息 - 再请求一次模型 返回的仍然是: - 模型文本 下一步: - 进入统一 JSON 解析器 ### 第 6 步:把模型输出解析成结构化结果 代码位置: - [parse_ai_json_response](file:///Users/fanhong/UGIT/shudao-aichat/app/utils/json_parser.py#L61-L113) - 调用点 [intent.py](file:///Users/fanhong/UGIT/shudao-aichat/app/api/intent.py#L300-L318) 这一层返回的是一个结构化字典,可能包含: - `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](file:///Users/fanhong/UGIT/shudao-aichat/app/api/intent.py#L105-L186) 这一层返回的是: - `thinking_content` 它不是原始推理链,而是安全可展示的摘要。 下一步: - 如果前面失败太多,就走本地兜底;否则直接组装响应 ### 第 8 步:如果模型不可靠,就走本地兜底 代码位置: - 兜底函数 [_build_fallback_intent_result](file:///Users/fanhong/UGIT/shudao-aichat/app/api/intent.py#L189-L234) - 触发点 [intent.py](file:///Users/fanhong/UGIT/shudao-aichat/app/api/intent.py#L320-L327) 返回的是: - 一份简化版意图识别结果 比如: - 是否专业问题 - 走在线还是离线 - 默认关键词 下一步: - 统一组装接口响应 ### 第 9 步:返回标准化的意图识别结果 代码位置: - [intent.py](file:///Users/fanhong/UGIT/shudao-aichat/app/api/intent.py#L328-L372) - 响应模型 [IntentAnalyzeResponse](file:///Users/fanhong/UGIT/shudao-aichat/app/schemas/models.py#L15-L29) 最终返回给上层的是: - `is_professional_question` - `route` - `keywords` - `fallback_keywords` - `intent_scene` - `company_name` - `company_aliases` - `summary` - `thinking_content` 下一步: - 交给报告主流程继续判断往哪走 --- ## 3.2 报告主流程怎么接这份意图结果 代码位置: - [report.py](file:///Users/fanhong/UGIT/shudao-aichat/app/api/report.py#L489-L659) ### 第 1 步:报告主流程先调用意图识别 调用点: - [report.py](file:///Users/fanhong/UGIT/shudao-aichat/app/api/report.py#L489-L527) 传进去的是: - 用户问题 - 对话历史 - 在线模型开关 返回的是: - `IntentAnalyzeResponse` 下一步: - 根据 `route` 和 `is_professional_question` 分支 ### 第 2 步:如果是 `online_only(仅在线)` 代码位置: - [report.py](file:///Users/fanhong/UGIT/shudao-aichat/app/api/report.py#L577-L604) 下一步: - 直接去在线回答流程 ### 第 3 步:如果是 `online_then_offline(先在线后离线)` 代码位置: - [report.py](file:///Users/fanhong/UGIT/shudao-aichat/app/api/report.py#L606-L608) 下一步: - 一边走在线回答 - 一边继续离线检索 / 报告生成 ### 第 4 步:如果不是专业问题 代码位置: - [report.py](file:///Users/fanhong/UGIT/shudao-aichat/app/api/report.py#L610-L617) 下一步: - 直接停止后面的专业检索流程 ### 第 5 步:如果是 `internal_query(内部查询)` 代码位置: - [report.py](file:///Users/fanhong/UGIT/shudao-aichat/app/api/report.py#L650-L659) 下一步: - 走内部文档检索分支 --- ## 4. 你最该记住的“问题流向” 如果只保留最简版,可以这样记: ### `shudao-chat-py` ```text 用户问题 -> 聊天路由 -> 意图识别小模型 -> 返回 intent_type -> 如果需要查库,就去 RAG -> 把问题 + RAG结果 传给主模型 -> 主模型生成最终回答 -> 如果有 think,再做思考摘要 -> 返回前端 ``` ### `shudao-chat-py` 主聊天接口 `stream/chat-with-db` ```text 用户问题 -> 创建/复用会话 -> 写 user 消息 -> 写 ai 占位消息 -> 直接去 RAG -> 拼历史上下文 -> 传给主模型流式生成 -> 边输出边写回数据库 ``` ### `shudao-aichat` ```text 用户问题 -> 结构化意图识别 -> 返回 route / keywords / scene / summary -> 报告主流程读取这些字段 -> 决定下一步走在线、离线、内部查询还是直接结束 ``` --- ## 5. 最后一句话 如果你问的是: - “用户的问题最后是怎么变成回答的?” 那最直接的答案就是: ### 在 `shudao-chat-py` - 用户问题先可能去“意图识别” - 再可能去“RAG 检索” - 然后一定会去“主模型生成最终回答” ### 在 `shudao-aichat` - 用户问题先去“结构化意图识别” - 这个步骤先不直接给最终答案 - 它先返回“下一步该怎么走” - 然后上层流程再继续往下跑