# AI助手模块 API 调用链路(详细版) 本文档从“前端发起一次 AI 问答”开始,梳理本项目 AI 问答模块涉及的 **入站接口(前端 → shudao-chat-py)** 与 **出站接口(shudao-chat-py → 外部服务)**,并按不同交互场景给出完整调用链路与时序图。 ## 0. 名词与范围 - **前端**:`shudao-vue-frontend` - **后端**:`shudao-chat-py`(FastAPI) - **aichat**:独立服务 `shudao-aichat`(本项目通过代理/兼容路由调用) - **搜索服务**:RAG 检索服务(本项目通过 HTTP 调用) - **Dify 工作流**:在线搜索链路使用 - **Qwen3**:主模型接口(OpenAI 风格 `/v1/chat/completions`) - **DeepSeek**:备用模型(Qwen3 异常时回退) - **4A Auth**:外部 token 校验接口 ## 1. 入站接口清单(前端 → shudao-chat-py) 下表只列 AI 问答相关;其他业务接口(考试工坊/PPT/隐患识别等)不在本文范围内。 | 场景 | 方法&路径(前缀 `/apiv1`) | 作用 | 前端调用点(示例) | 后端实现 | |---|---|---|---|---| | 主流式问答(写 DB) | `POST /stream/chat-with-db` | SSE 流式输出 + 落库 | [Chat.vue](file:///Users/fanhong/UGIT/shudao-main/shudao-vue-frontend/src/views/Chat.vue#L2398-L2498) | [stream_chat_with_db](file:///Users/fanhong/UGIT/shudao-main/shudao-chat-py/routers/chat.py#L1491-L1804) | | 非流式问答 | `POST /send_deepseek_message` | 一次性返回完整回答 | [Chat.vue](file:///Users/fanhong/UGIT/shudao-main/shudao-vue-frontend/src/views/Chat.vue#L2512-L2630)、[apis.js](file:///Users/fanhong/UGIT/shudao-main/shudao-vue-frontend/src/request/apis.js#L17-L19) | [send_deepseek_message](file:///Users/fanhong/UGIT/shudao-main/shudao-chat-py/routers/chat.py#L893-L1183) | | 历史(列表/详情) | `GET /get_history_record` | `ai_conversation_id=0` 返回会话列表;`>0` 返回消息列表 | [apis.js](file:///Users/fanhong/UGIT/shudao-main/shudao-vue-frontend/src/request/apis.js#L38-L40) | [get_history_record](file:///Users/fanhong/UGIT/shudao-main/shudao-chat-py/routers/chat.py#L1185-L1266) | | 删除对话/消息 | `POST /delete_conversation` | 软删除会话或某条消息对 | [apis.js](file:///Users/fanhong/UGIT/shudao-main/shudao-vue-frontend/src/request/apis.js#L71-L76) | [delete_conversation](file:///Users/fanhong/UGIT/shudao-main/shudao-chat-py/routers/chat.py#L1268-L1333) | | 删除历史(会话级) | `POST /delete_history_record` | 软删除会话主记录 | [apis.js](file:///Users/fanhong/UGIT/shudao-main/shudao-vue-frontend/src/request/apis.js#L74-L76) | [delete_history_record](file:///Users/fanhong/UGIT/shudao-main/shudao-chat-py/routers/chat.py#L1335-L1353) | | 猜你想问 | `POST /guess_you_want` | 基于某条 AI 消息生成 3 个关联问题并落库 | [apis.js](file:///Users/fanhong/UGIT/shudao-main/shudao-vue-frontend/src/request/apis.js#L116-L118) | [guess_you_want](file:///Users/fanhong/UGIT/shudao-main/shudao-chat-py/routers/chat.py#L1811-L1885) | | 在线搜索 | `GET /online_search?question=...` | Qwen 提炼关键词 → Dify 工作流 → 返回摘要 | [apis.js](file:///Users/fanhong/UGIT/shudao-main/shudao-vue-frontend/src/request/apis.js#L101-L105) | [online_search](file:///Users/fanhong/UGIT/shudao-main/shudao-chat-py/routers/chat.py#L1891-L1942) | | 保存在线搜索结果 | `POST /save_online_search_result` | 保存到 `AIMessage.search_source` | [apis.js](file:///Users/fanhong/UGIT/shudao-main/shudao-vue-frontend/src/request/apis.js#L104-L106) | [save_online_search_result](file:///Users/fanhong/UGIT/shudao-main/shudao-chat-py/routers/chat.py#L1944-L1970) | | 意图识别(独立) | `POST /intent_recognition` | 只做意图识别(可选写 DB) | [apis.js](file:///Users/fanhong/UGIT/shudao-main/shudao-vue-frontend/src/request/apis.js#L107-L109) | [intent_recognition](file:///Users/fanhong/UGIT/shudao-main/shudao-chat-py/routers/chat.py#L1976-L2072) | | 点赞/点踩 | `POST /like_and_dislike` | 保存 `AIMessage.user_feedback` 并处理积分 | [apis.js](file:///Users/fanhong/UGIT/shudao-main/shudao-vue-frontend/src/request/apis.js#L58-L60) | [like_and_dislike](file:///Users/fanhong/UGIT/shudao-main/shudao-chat-py/routers/total.py#L226-L275) | | 报告兼容 SSE(Go 对齐) | `POST /report/complete-flow` | SSE:可能代理到 aichat;失败则降级本地 | [Chat.vue](file:///Users/fanhong/UGIT/shudao-main/shudao-vue-frontend/src/views/Chat.vue#L3744-L3830) | [complete_flow](file:///Users/fanhong/UGIT/shudao-main/shudao-chat-py/routers/report_compat.py#L187-L234) | | 停止报告 SSE | `POST /sse/stop` | 停止 SSE(外部 token 代理到 aichat) | [stopSSEStream](file:///Users/fanhong/UGIT/shudao-main/shudao-vue-frontend/src/utils/api.js#L82-L140) | [stop_sse](file:///Users/fanhong/UGIT/shudao-main/shudao-chat-py/routers/report_compat.py#L302-L337) | | 回写 AI 消息 | `POST /report/update-ai-message` | 前端将整理后的内容回写到 DB(外部 token 代理到 aichat) | [updateAIMessageContent](file:///Users/fanhong/UGIT/shudao-main/shudao-vue-frontend/src/utils/api.js#L147-L205) | [update_ai_message](file:///Users/fanhong/UGIT/shudao-main/shudao-chat-py/routers/report_compat.py#L246-L299) | | 附件解析 | `POST /attachments/parse` | 文件上传解析(代理到 aichat) | [parseAttachment](file:///Users/fanhong/UGIT/shudao-main/shudao-vue-frontend/src/request/apis.js#L27-L29) | [parse_attachment](file:///Users/fanhong/UGIT/shudao-main/shudao-chat-py/routers/report_compat.py#L236-L244) | ## 2. 出站接口清单(shudao-chat-py → 外部服务) | 目标服务 | 调用点 | 典型接口 | 用途 | |---|---|---|---| | 4A Auth | [verify_external_token](file:///Users/fanhong/UGIT/shudao-main/shudao-chat-py/utils/token.py#L175-L228) | `POST settings.auth.api_url` | 校验外部 token,解析 account 信息 | | 搜索服务(RAG) | [_rag_search](file:///Users/fanhong/UGIT/shudao-main/shudao-chat-py/routers/chat.py#L529-L553) | `POST settings.search.api_url` | 向量检索/知识库检索,拼接上下文 | | Qwen3(主模型) | [QwenService.chat](file:///Users/fanhong/UGIT/shudao-main/shudao-chat-py/services/qwen_service.py#L117-L207)、[QwenService.stream_chat](file:///Users/fanhong/UGIT/shudao-main/shudao-chat-py/services/qwen_service.py#L208-L256) | `POST {settings.qwen3.api_url}/v1/chat/completions` | 主问答生成与流式输出 | | 意图识别模型 | [intent_recognition](file:///Users/fanhong/UGIT/shudao-main/shudao-chat-py/services/qwen_service.py#L73-L116) | `POST {settings.intent.api_url}/v1/chat/completions` | 识别 greeting/faq/知识库查询 等 | | DeepSeek(备用模型) | [DeepSeekService.chat](file:///Users/fanhong/UGIT/shudao-main/shudao-chat-py/services/deepseek_service.py#L24-L48)、[stream_chat](file:///Users/fanhong/UGIT/shudao-main/shudao-chat-py/services/deepseek_service.py#L50-L89) | `POST {settings.deepseek.api_url}/v1/chat/completions` | Qwen3 失败时回退 | | Dify 工作流(在线搜索) | [online_search](file:///Users/fanhong/UGIT/shudao-main/shudao-chat-py/routers/chat.py#L1891-L1942) | `POST settings.dify.workflow_url` | 关键词 → 工作流 → 摘要 | | aichat 服务(代理) | [AIChatProxy](file:///Users/fanhong/UGIT/shudao-main/shudao-chat-py/services/aichat_proxy.py#L13-L207) | `POST {settings.aichat.api_url}/*` | 兼容 Go 版报告 SSE、附件解析、停止、回写等 | ## 3. 认证与用户注入(所有链路的共同前置) ### 3.1 中间件入口 后端在 [combined_middleware](file:///Users/fanhong/UGIT/shudao-main/shudao-chat-py/main.py#L33-L107) 内做: 1. 从 `Authorization` / `token` / `Token` 读取 token 2. 调用 [verify_token](file:///Users/fanhong/UGIT/shudao-main/shudao-chat-py/utils/token.py#L230-L243) 3. 校验成功后写入 `request.state.user` 4. 路由层统一通过 `request.state.user` 获取当前用户 ### 3.2 外部 token 的校验链路 外部 token: 1. `POST settings.auth.api_url` 验证 token 合法性 2. 从返回数据取 `accountID / name / role / exp` 3. 调用 [_resolve_external_user_id](file:///Users/fanhong/UGIT/shudao-main/shudao-chat-py/utils/token.py#L149-L173) 映射到本地 `UserData.id`(用于复用 `AIConversation/AIMessage`) ## 4. 链路 A:主流式问答(`POST /stream/chat-with-db`) 这是“边生成边显示 + 保留历史”的标准链路。 ### 4.1 请求形态 请求体(核心字段): ```json { "message": "用户问题", "ai_conversation_id": 0, "business_type": 0, "online_search_content": "" } ``` 响应:`text/event-stream`,每条事件形态为: - 首次:`data: {"type":"initial","ai_conversation_id":123,"ai_message_id":456}\n\n` - 中间:`data: <文本chunk>\n\n`(chunk 内换行会被转义为 `\\n`) - 结束:`data: [DONE]\n\n` 实现见 [stream_chat_with_db](file:///Users/fanhong/UGIT/shudao-main/shudao-chat-py/routers/chat.py#L1491-L1804)。 ### 4.2 详细调用链路(含 DB 与外部 API) 1. 前端 → `POST /apiv1/stream/chat-with-db` 2. 中间件: - 校验 token(本地 / 外部 4A) - 写入 `request.state.user` 3. 路由进入 `stream_chat_with_db`: - 创建/复用 `AIConversation` - 写入 `AIMessage(type=user)` - 写入 `AIMessage(type=ai, content='')` 占位 - SSE 推送 `initial`(返回会话/消息 ID) 4. 触发 RAG: - `POST settings.search.api_url`,请求体 `{"query": message, "n_results": top_k}` - 将结果拼成 `rag_context` 5. 拼装 Prompt: - `final_answer`(含 userMessage、rag_context、historyContext、可选 online_search_content) 6. 调用主模型流式生成: - `POST {settings.qwen3.api_url}/v1/chat/completions`(`stream=true`) - 若 Qwen3 上游错误或网络错误,回退到 DeepSeek 流式接口 7. 可选:思考过程摘要(二次模型调用) - 若流中出现 `...` 且摘要开关启用,会调用 [summarize_thinking_content](file:///Users/fanhong/UGIT/shudao-main/shudao-chat-py/utils/thinking_summary.py#L321-L397) - 该摘要本质上是一次 `qwen_service.chat()`(仍会触发 DeepSeek 回退逻辑) 8. SSE 持续返回给前端 9. 流结束后回写数据库: - 更新 `AIMessage(type=ai).content = full_response` - 更新 `AIConversation.updated_at / business_type / content 预览` 10. SSE 返回 `[DONE]` ### 4.3 时序图(主流式问答) ```mermaid sequenceDiagram participant FE as 前端 participant MW as 中间件 participant AU as 4A Auth participant RT as chat.py:/stream/chat-with-db participant DB as MySQL participant SS as 搜索服务 participant QW as Qwen3 participant DS as DeepSeek(回退) FE->>MW: POST /apiv1/stream/chat-with-db MW->>AU: (可选) POST settings.auth.api_url AU-->>MW: valid + account MW-->>RT: request.state.user RT->>DB: upsert AIConversation RT->>DB: insert user AIMessage RT->>DB: insert ai placeholder AIMessage RT-->>FE: SSE data: {"type":"initial",...} RT->>SS: POST settings.search.api_url SS-->>RT: docs RT->>QW: POST /v1/chat/completions (stream=true) alt Qwen3 失败 RT->>DS: POST /v1/chat/completions (stream=true) DS-->>RT: chunk... else Qwen3 正常 QW-->>RT: chunk... end RT-->>FE: SSE chunk... RT->>DB: update ai_message.content RT->>DB: update conversation snapshot RT-->>FE: SSE [DONE] ``` ## 5. 链路 B:非流式问答(`POST /send_deepseek_message`,`business_type=0`) 这条链路更像“同步 RPC”:一次请求直接拿到完整回答。 ### 5.1 调用链路(AI助手分支) 实现入口见 [send_deepseek_message](file:///Users/fanhong/UGIT/shudao-main/shudao-chat-py/routers/chat.py#L893-L1183)。 1. 前端 → `POST /apiv1/send_deepseek_message` 2. 中间件鉴权,写入 `request.state.user` 3. 创建/复用 `AIConversation` 4. 意图识别: - 调用 [QwenService.intent_recognition](file:///Users/fanhong/UGIT/shudao-main/shudao-chat-py/services/qwen_service.py#L73-L116) - 出站:`POST {settings.intent.api_url}/v1/chat/completions` 5. 若意图命中“知识库查询/技术咨询”,触发 RAG: - `POST settings.search.api_url` 6. 组装 `final_answer` Prompt 7. 非流式生成: - `POST {settings.qwen3.api_url}/v1/chat/completions`(`stream=false`) - Qwen3 可回退 DeepSeek(见 [QwenService.chat](file:///Users/fanhong/UGIT/shudao-main/shudao-chat-py/services/qwen_service.py#L117-L207)) 8. 可选:思考过程摘要(再次调用 `qwen_service.chat()`) 9. 返回 `{statusCode: 200, data: { reply/content/... } }` ### 5.2 时序图(非流式问答) ```mermaid sequenceDiagram participant FE as 前端 participant MW as 中间件 participant RT as chat.py:/send_deepseek_message participant IM as Intent模型 participant SS as 搜索服务 participant QW as Qwen3 participant DS as DeepSeek(回退) participant DB as MySQL FE->>MW: POST /apiv1/send_deepseek_message MW-->>RT: request.state.user RT->>DB: upsert AIConversation RT->>IM: POST intent /v1/chat/completions IM-->>RT: intent_type opt 命中知识库查询 RT->>SS: POST settings.search.api_url SS-->>RT: docs end RT->>QW: POST qwen3 /v1/chat/completions (stream=false) alt Qwen3 失败 RT->>DS: POST deepseek /v1/chat/completions DS-->>RT: answer else 正常 QW-->>RT: answer end RT-->>FE: JSON response(一次性) ``` ## 6. 链路 C:报告兼容 SSE(`POST /report/complete-flow`) 这条链路是为了对齐 Go 版本接口格式,前端在“报告生成模式”会调用它(见 [Chat.vue](file:///Users/fanhong/UGIT/shudao-main/shudao-vue-frontend/src/views/Chat.vue#L3744-L3830))。 后端入口: [complete_flow](file:///Users/fanhong/UGIT/shudao-main/shudao-chat-py/routers/report_compat.py#L187-L234) ### 6.1 两种路由策略(关键) 兼容路由会根据 token 类型决定“走 aichat 代理”还是“本地降级”: - **外部 token(非本地 token)**:优先代理到 aichat 判断逻辑: [should_proxy_to_aichat](file:///Users/fanhong/UGIT/shudao-main/shudao-chat-py/routers/report_compat.py#L39-L46) - **本地 token** 或 **代理失败**:降级为本地流式 `POST /stream/chat-with-db` 再转换为兼容事件流 降级逻辑: [fallback_to_local_stream](file:///Users/fanhong/UGIT/shudao-main/shudao-chat-py/routers/report_compat.py#L93-L185) ### 6.2 外部 token:代理到 aichat 链路: 1. 前端 → `POST /apiv1/report/complete-flow`(SSE) 2. `report_compat.py` 构造转发请求体,规范化 `ai_conversation_id`(新会话必须是 0) 3. 调用代理: [aichat_proxy.proxy_sse](file:///Users/fanhong/UGIT/shudao-main/shudao-chat-py/services/aichat_proxy.py#L32-L95) 4. 后端将 aichat SSE 原样转发给前端(chunk 级别) 其中 aichat 的 base URL 来自: - `settings.aichat.api_url`(见 [AIChatConfig](file:///Users/fanhong/UGIT/shudao-main/shudao-chat-py/utils/config.py#L66-L69)) ### 6.3 本地 token:降级走本地流式并“翻译事件” 链路: 1. 前端 → `POST /apiv1/report/complete-flow`(SSE) 2. 进入 `fallback_to_local_stream` 3. 后端内部再次发起 HTTP 调用: - `POST http://127.0.0.1:{settings.app.port}/apiv1/stream/chat-with-db` 4. 从本地 SSE 中解析: - 读取 `initial` 获取 `ai_conversation_id / ai_message_id` - 将后续 chunk 累积为 `full_response` 5. 当收到 `[DONE]` 时,输出兼容事件: - `data: {"type":"online_answer","content": full_response, ...}\n\n` - `data: {"type":"completed"}\n\n` ### 6.4 时序图(报告兼容 SSE) ```mermaid sequenceDiagram participant FE as 前端 participant RC as report_compat:/report/complete-flow participant AP as aichat_proxy participant AC as aichat 服务 participant LC as 本地 chat-with-db FE->>RC: POST /apiv1/report/complete-flow (SSE) alt 外部 token RC->>AP: proxy_sse("/report/complete-flow") AP->>AC: POST {settings.aichat.api_url}/report/complete-flow (SSE) AC-->>AP: SSE chunk... AP-->>FE: SSE chunk...(原样转发) else 本地 token 或代理失败 RC->>LC: POST http://127.0.0.1:{port}/apiv1/stream/chat-with-db (SSE) LC-->>RC: SSE initial + chunk... + [DONE] RC-->>FE: data: {"type":"online_answer",...} RC-->>FE: data: {"type":"completed"} end ``` ## 7. 在线搜索链路(`GET /online_search` + `POST /save_online_search_result`) 在线搜索用于补充回答来源或做联网摘要。 ### 7.1 `GET /online_search` 实现入口: [online_search](file:///Users/fanhong/UGIT/shudao-main/shudao-chat-py/routers/chat.py#L1891-L1942) 调用链路: 1. 前端 → `GET /apiv1/online_search?question=...` 2. 后端用 Qwen 提炼关键词: - [extract_keywords](file:///Users/fanhong/UGIT/shudao-main/shudao-chat-py/services/qwen_service.py#L45-L71) 3. 后端调用 Dify workflow: - `POST settings.dify.workflow_url` - `Authorization: Bearer settings.dify.auth_token` 4. 返回 `{keywords, result}` 给前端 ### 7.2 `POST /save_online_search_result` 实现入口: [save_online_search_result](file:///Users/fanhong/UGIT/shudao-main/shudao-chat-py/routers/chat.py#L1944-L1970) 调用链路: 1. 前端提交 `{ai_message_id, search_result}` 2. 后端更新 `AIMessage.search_source` ## 8. “猜你想问”链路(`POST /guess_you_want`) 实现入口: [guess_you_want](file:///Users/fanhong/UGIT/shudao-main/shudao-chat-py/routers/chat.py#L1811-L1885) 调用链路: 1. 前端提交 `{ai_message_id}` 2. 后端读取该条 AI 消息内容 3. 后端加载 `guess_questions` prompt 并调用 `qwen_service.chat()` 4. 将结果解析为 `questions[]`,写入 `AIMessage.guess_you_want` 5. 返回 `{ai_message_id, questions}` ## 9. 附件解析链路(`POST /attachments/parse`) 实现入口: [parse_attachment](file:///Users/fanhong/UGIT/shudao-main/shudao-chat-py/routers/report_compat.py#L236-L244) 调用链路: 1. 前端上传文件(multipart/form-data) 2. 后端校验 `request.state.user` 不为空 3. 代理到 aichat: - [proxy_upload](file:///Users/fanhong/UGIT/shudao-main/shudao-chat-py/services/aichat_proxy.py#L147-L192) - `POST {settings.aichat.api_url}/attachments/parse` 4. 返回解析结果给前端(通常含抽取文本/attachment_id 等) ## 10. 停止 SSE 与回写消息(报告模式常用) ### 10.1 `POST /sse/stop` 前端调用: [stopSSEStream](file:///Users/fanhong/UGIT/shudao-main/shudao-vue-frontend/src/utils/api.js#L82-L140) 后端实现: [stop_sse](file:///Users/fanhong/UGIT/shudao-main/shudao-chat-py/routers/report_compat.py#L302-L337) 路由策略: - 外部 token:代理到 aichat 的 `/sse/stop` - 本地 token:本地直接返回成功(不做真实中断) ### 10.2 `POST /report/update-ai-message` 前端调用: [updateAIMessageContent](file:///Users/fanhong/UGIT/shudao-main/shudao-vue-frontend/src/utils/api.js#L147-L205) 后端实现: [update_ai_message](file:///Users/fanhong/UGIT/shudao-main/shudao-chat-py/routers/report_compat.py#L246-L299) 路由策略: - 外部 token:代理到 aichat 的 `/report/update-ai-message` - 本地 token:直接更新 `AIMessage.id = ai_message_id` 的 `content` ## 11. 相关配置入口(定位“外部服务地址”用) 统一配置类定义在: - [config.py](file:///Users/fanhong/UGIT/shudao-main/shudao-chat-py/utils/config.py) 通常运行时值来自 `shudao-chat-py/config.yaml`(以及环境变量覆盖),关键项包括: - `auth.api_url` - `search.api_url` - `qwen3.api_url` / `qwen3.model` - `intent.api_url` / `intent.model` - `deepseek.api_url` - `dify.workflow_url` / `dify.workflow_id` / `dify.auth_token` - `aichat.api_url`