# 用户提问后,问题会被传去哪儿
这份文档不再按“模块设计”讲,而是按你最关心的方式讲:
- 用户问了一个问题
- 这个问题先传到哪里
- 那一层返回什么
- 下一步再传到哪里
当前项目里,主要有两条链路:
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`
- 用户问题先去“结构化意图识别”
- 这个步骤先不直接给最终答案
- 它先返回“下一步该怎么走”
- 然后上层流程再继续往下跑