资源下载链接:LangChain ReAct Agent 智能客服系统(扫地机器人垂直场景)
说明:本文档描述项目当前整体现状,涵盖架构、模块、存储、配置、部署、接口与扩展方式。
1. 项目概述
1.1 项目定位
基于 LangChain + LangGraph 构建的生产级 ReAct 智能客服 Agent,以"扫地机器人产品咨询"为示例垂直场景,支持三类核心任务:
| 任务类型 | 说明 | 依赖工具 |
|---|---|---|
| 知识库问答 | 故障咨询、操作说明、保养方法等产品问题 | rag_summarize(RAG 检索增强) |
| 工具辅助问答 | 天气查询、用户定位等外部信息补充 | get_weather / get_user_location |
| 个性化报告生成 | 按用户 + 月份生成使用分析报告 | fill_context_for_report + fetch_external_data |
1.2 核心能力
- ReAct 多工具智能体 :基于 LangGraph
create_agent高层 API,模型自主决策"思考 → 工具调用 → 观察 → 再思考",支持动态提示词切换; - RAG 两阶段检索:ES 向量 + BM25 混合检索(RRF 融合)粗召回 → Rerank 精排 + 阈值过滤 → LLM 生成,附参考资料来源(metadata 透传);
- Redis 持久化会话:多用户、多会话长期对话,7 天 TTL,LIST 原子追加防并发丢消息;
- RESTful 流式后端 :FastAPI + SSE(
text/event-stream)流式返回,支持第三方系统对接与独立部署; - Agent 高可用防护:迭代轮数熔断、原地打转检测、图级 recursion_limit 硬上限、工具超时兜底、全链路降级;
- LLM 自动化评测:测试用例 + LLM 裁判打分,输出任务成功率 / 工具调用准确率 / 幻觉率 / 平均分。
1.3 技术栈
| 层 | 技术选型 |
|---|---|
| Agent 框架 | LangChain 1.2.x、LangGraph(create_agent 高层 API、stream_mode="values") |
| 大模型 | 阿里云百炼 DashScope:qwen3-max(对话)、text-embedding-v4(向量)、qwen3-rerank(精排 API) |
| 服务框架 | FastAPI 0.141.x + uvicorn + SSE |
| 会话存储 | Redis(LIST 结构 + pipeline 原子操作) |
| 向量检索 | Elasticsearch 8.x(生产主后端:向量 KNN + BM25 混合检索) |
| 降级向量库 | Chroma(ES 故障 / 非 ES 后端时自动降级) |
| 前端(可选) | Streamlit(app_streamlit.py,本地调试界面) |
| 评测 | 自研 eval/eval_runner.py(LLM 裁判) |
2. 系统架构
2.1 总体架构
#mermaid-svg-iOY9LndcZXAEol4S{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-iOY9LndcZXAEol4S .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-iOY9LndcZXAEol4S .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-iOY9LndcZXAEol4S .error-icon{fill:#552222;}#mermaid-svg-iOY9LndcZXAEol4S .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-iOY9LndcZXAEol4S .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-iOY9LndcZXAEol4S .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-iOY9LndcZXAEol4S .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-iOY9LndcZXAEol4S .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-iOY9LndcZXAEol4S .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-iOY9LndcZXAEol4S .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-iOY9LndcZXAEol4S .marker{fill:#333333;stroke:#333333;}#mermaid-svg-iOY9LndcZXAEol4S .marker.cross{stroke:#333333;}#mermaid-svg-iOY9LndcZXAEol4S svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-iOY9LndcZXAEol4S p{margin:0;}#mermaid-svg-iOY9LndcZXAEol4S .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-iOY9LndcZXAEol4S .cluster-label text{fill:#333;}#mermaid-svg-iOY9LndcZXAEol4S .cluster-label span{color:#333;}#mermaid-svg-iOY9LndcZXAEol4S .cluster-label span p{background-color:transparent;}#mermaid-svg-iOY9LndcZXAEol4S .label text,#mermaid-svg-iOY9LndcZXAEol4S span{fill:#333;color:#333;}#mermaid-svg-iOY9LndcZXAEol4S .node rect,#mermaid-svg-iOY9LndcZXAEol4S .node circle,#mermaid-svg-iOY9LndcZXAEol4S .node ellipse,#mermaid-svg-iOY9LndcZXAEol4S .node polygon,#mermaid-svg-iOY9LndcZXAEol4S .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-iOY9LndcZXAEol4S .rough-node .label text,#mermaid-svg-iOY9LndcZXAEol4S .node .label text,#mermaid-svg-iOY9LndcZXAEol4S .image-shape .label,#mermaid-svg-iOY9LndcZXAEol4S .icon-shape .label{text-anchor:middle;}#mermaid-svg-iOY9LndcZXAEol4S .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-iOY9LndcZXAEol4S .rough-node .label,#mermaid-svg-iOY9LndcZXAEol4S .node .label,#mermaid-svg-iOY9LndcZXAEol4S .image-shape .label,#mermaid-svg-iOY9LndcZXAEol4S .icon-shape .label{text-align:center;}#mermaid-svg-iOY9LndcZXAEol4S .node.clickable{cursor:pointer;}#mermaid-svg-iOY9LndcZXAEol4S .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-iOY9LndcZXAEol4S .arrowheadPath{fill:#333333;}#mermaid-svg-iOY9LndcZXAEol4S .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-iOY9LndcZXAEol4S .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-iOY9LndcZXAEol4S .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-iOY9LndcZXAEol4S .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-iOY9LndcZXAEol4S .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-iOY9LndcZXAEol4S .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-iOY9LndcZXAEol4S .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-iOY9LndcZXAEol4S .cluster text{fill:#333;}#mermaid-svg-iOY9LndcZXAEol4S .cluster span{color:#333;}#mermaid-svg-iOY9LndcZXAEol4S div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-iOY9LndcZXAEol4S .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-iOY9LndcZXAEol4S rect.text{fill:none;stroke-width:0;}#mermaid-svg-iOY9LndcZXAEol4S .icon-shape,#mermaid-svg-iOY9LndcZXAEol4S .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-iOY9LndcZXAEol4S .icon-shape p,#mermaid-svg-iOY9LndcZXAEol4S .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-iOY9LndcZXAEol4S .icon-shape .label rect,#mermaid-svg-iOY9LndcZXAEol4S .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-iOY9LndcZXAEol4S .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-iOY9LndcZXAEol4S .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-iOY9LndcZXAEol4S :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 基础设施
RAG 层 rag/
Agent 层 agent/
API 服务层 api/main.py
客户端
Streamlit 前端 / 第三方系统
POST /api/v1/chat/stream (SSE)
GET /api/v1/chat/history
GET /health
CORS 白名单 + 入参校验
ReactAgent
create_agent 高层 API
中间件 middleware.py
monitor_tool / 动态提示词
工具集 agent_tools.py
rag_summarize / fetch_external_data ...
工具超时装饰器
tool_timeout
VectorStoreService
hybrid_search 混合检索
RerankService
精排 + 阈值过滤
RagSummarizeService
LCEL 链路组装
Redis
chat:msg:{sid} / chat:ctx:{sid}
Elasticsearch
rag_knowledge 索引
Chroma
降级向量库
DashScope API
qwen3-max / embedding / rerank
2.2 对话请求时序(SSE 流式)
ES / Rerank / LLM Redis ReactAgent api/main.py 客户端 ES / Rerank / LLM Redis ReactAgent api/main.py 客户端 #mermaid-svg-Bbe5LmwRuhbmurRv{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-Bbe5LmwRuhbmurRv .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-Bbe5LmwRuhbmurRv .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-Bbe5LmwRuhbmurRv .error-icon{fill:#552222;}#mermaid-svg-Bbe5LmwRuhbmurRv .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-Bbe5LmwRuhbmurRv .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-Bbe5LmwRuhbmurRv .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-Bbe5LmwRuhbmurRv .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-Bbe5LmwRuhbmurRv .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-Bbe5LmwRuhbmurRv .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-Bbe5LmwRuhbmurRv .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-Bbe5LmwRuhbmurRv .marker{fill:#333333;stroke:#333333;}#mermaid-svg-Bbe5LmwRuhbmurRv .marker.cross{stroke:#333333;}#mermaid-svg-Bbe5LmwRuhbmurRv svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-Bbe5LmwRuhbmurRv p{margin:0;}#mermaid-svg-Bbe5LmwRuhbmurRv .actor{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-Bbe5LmwRuhbmurRv text.actor>tspan{fill:black;stroke:none;}#mermaid-svg-Bbe5LmwRuhbmurRv .actor-line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-Bbe5LmwRuhbmurRv .innerArc{stroke-width:1.5;stroke-dasharray:none;}#mermaid-svg-Bbe5LmwRuhbmurRv .messageLine0{stroke-width:1.5;stroke-dasharray:none;stroke:#333;}#mermaid-svg-Bbe5LmwRuhbmurRv .messageLine1{stroke-width:1.5;stroke-dasharray:2,2;stroke:#333;}#mermaid-svg-Bbe5LmwRuhbmurRv #arrowhead path{fill:#333;stroke:#333;}#mermaid-svg-Bbe5LmwRuhbmurRv .sequenceNumber{fill:white;}#mermaid-svg-Bbe5LmwRuhbmurRv #sequencenumber{fill:#333;}#mermaid-svg-Bbe5LmwRuhbmurRv #crosshead path{fill:#333;stroke:#333;}#mermaid-svg-Bbe5LmwRuhbmurRv .messageText{fill:#333;stroke:none;}#mermaid-svg-Bbe5LmwRuhbmurRv .labelBox{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-Bbe5LmwRuhbmurRv .labelText,#mermaid-svg-Bbe5LmwRuhbmurRv .labelText>tspan{fill:black;stroke:none;}#mermaid-svg-Bbe5LmwRuhbmurRv .loopText,#mermaid-svg-Bbe5LmwRuhbmurRv .loopText>tspan{fill:black;stroke:none;}#mermaid-svg-Bbe5LmwRuhbmurRv .loopLine{stroke-width:2px;stroke-dasharray:2,2;stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-Bbe5LmwRuhbmurRv .note{stroke:#aaaa33;fill:#fff5ad;}#mermaid-svg-Bbe5LmwRuhbmurRv .noteText,#mermaid-svg-Bbe5LmwRuhbmurRv .noteText>tspan{fill:black;stroke:none;}#mermaid-svg-Bbe5LmwRuhbmurRv .activation0{fill:#f4f4f4;stroke:#666;}#mermaid-svg-Bbe5LmwRuhbmurRv .activation1{fill:#f4f4f4;stroke:#666;}#mermaid-svg-Bbe5LmwRuhbmurRv .activation2{fill:#f4f4f4;stroke:#666;}#mermaid-svg-Bbe5LmwRuhbmurRv .actorPopupMenu{position:absolute;}#mermaid-svg-Bbe5LmwRuhbmurRv .actorPopupMenuPanel{position:absolute;fill:#ECECFF;box-shadow:0px 8px 16px 0px rgba(0,0,0,0.2);filter:drop-shadow(3px 5px 2px rgb(0 0 0 / 0.4));}#mermaid-svg-Bbe5LmwRuhbmurRv .actor-man line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-Bbe5LmwRuhbmurRv .actor-man circle,#mermaid-svg-Bbe5LmwRuhbmurRv line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;stroke-width:2px;}#mermaid-svg-Bbe5LmwRuhbmurRv :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} loop 每帧 state POST /api/v1/chat/stream 入参校验(query 非空/长度/mode 枚举) get_msg(session_id) 单次读历史 set_context({report})(失败仅告警) execute_stream_with_raw(query, history_msg) 图执行(模型/工具节点) yield (state, out_text) 增量提取 reasoning(仅新增消息) SSE data:{type:"reasoning"} 熔断判断(轮数/重复调用) SSE data:{type:"final"} data:DONE append_turn(RPUSH+LTRIM+EXPIRE 原子落库)
2.3 RAG 检索链路
#mermaid-svg-7QLvAy9oq3KWmlHb{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-7QLvAy9oq3KWmlHb .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-7QLvAy9oq3KWmlHb .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-7QLvAy9oq3KWmlHb .error-icon{fill:#552222;}#mermaid-svg-7QLvAy9oq3KWmlHb .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-7QLvAy9oq3KWmlHb .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-7QLvAy9oq3KWmlHb .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-7QLvAy9oq3KWmlHb .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-7QLvAy9oq3KWmlHb .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-7QLvAy9oq3KWmlHb .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-7QLvAy9oq3KWmlHb .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-7QLvAy9oq3KWmlHb .marker{fill:#333333;stroke:#333333;}#mermaid-svg-7QLvAy9oq3KWmlHb .marker.cross{stroke:#333333;}#mermaid-svg-7QLvAy9oq3KWmlHb svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-7QLvAy9oq3KWmlHb p{margin:0;}#mermaid-svg-7QLvAy9oq3KWmlHb .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-7QLvAy9oq3KWmlHb .cluster-label text{fill:#333;}#mermaid-svg-7QLvAy9oq3KWmlHb .cluster-label span{color:#333;}#mermaid-svg-7QLvAy9oq3KWmlHb .cluster-label span p{background-color:transparent;}#mermaid-svg-7QLvAy9oq3KWmlHb .label text,#mermaid-svg-7QLvAy9oq3KWmlHb span{fill:#333;color:#333;}#mermaid-svg-7QLvAy9oq3KWmlHb .node rect,#mermaid-svg-7QLvAy9oq3KWmlHb .node circle,#mermaid-svg-7QLvAy9oq3KWmlHb .node ellipse,#mermaid-svg-7QLvAy9oq3KWmlHb .node polygon,#mermaid-svg-7QLvAy9oq3KWmlHb .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-7QLvAy9oq3KWmlHb .rough-node .label text,#mermaid-svg-7QLvAy9oq3KWmlHb .node .label text,#mermaid-svg-7QLvAy9oq3KWmlHb .image-shape .label,#mermaid-svg-7QLvAy9oq3KWmlHb .icon-shape .label{text-anchor:middle;}#mermaid-svg-7QLvAy9oq3KWmlHb .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-7QLvAy9oq3KWmlHb .rough-node .label,#mermaid-svg-7QLvAy9oq3KWmlHb .node .label,#mermaid-svg-7QLvAy9oq3KWmlHb .image-shape .label,#mermaid-svg-7QLvAy9oq3KWmlHb .icon-shape .label{text-align:center;}#mermaid-svg-7QLvAy9oq3KWmlHb .node.clickable{cursor:pointer;}#mermaid-svg-7QLvAy9oq3KWmlHb .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-7QLvAy9oq3KWmlHb .arrowheadPath{fill:#333333;}#mermaid-svg-7QLvAy9oq3KWmlHb .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-7QLvAy9oq3KWmlHb .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-7QLvAy9oq3KWmlHb .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-7QLvAy9oq3KWmlHb .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-7QLvAy9oq3KWmlHb .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-7QLvAy9oq3KWmlHb .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-7QLvAy9oq3KWmlHb .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-7QLvAy9oq3KWmlHb .cluster text{fill:#333;}#mermaid-svg-7QLvAy9oq3KWmlHb .cluster span{color:#333;}#mermaid-svg-7QLvAy9oq3KWmlHb div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-7QLvAy9oq3KWmlHb .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-7QLvAy9oq3KWmlHb rect.text{fill:none;stroke-width:0;}#mermaid-svg-7QLvAy9oq3KWmlHb .icon-shape,#mermaid-svg-7QLvAy9oq3KWmlHb .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-7QLvAy9oq3KWmlHb .icon-shape p,#mermaid-svg-7QLvAy9oq3KWmlHb .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-7QLvAy9oq3KWmlHb .icon-shape .label rect,#mermaid-svg-7QLvAy9oq3KWmlHb .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-7QLvAy9oq3KWmlHb .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-7QLvAy9oq3KWmlHb .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-7QLvAy9oq3KWmlHb :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} ES 异常自动降级
用户问题
hybrid_search 混合检索
ES 纯向量检索 top_k*2
ES 原生 BM25 检索 top_k*2
RRF 倒数排序融合
score = Σ 1/(rank+60)
截取 top_k 候选
Rerank 精排打分
阈值过滤(≥0.3)
截断 top_n=5
LLM 生成回答
(参考资料+元数据入上下文)
Chroma 纯向量检索
3. 目录结构
LangChain-ReAct-Agent/
├── api/
│ └── main.py # FastAPI 服务入口:SSE 流式接口 / 历史 / 健康检查 / CORS / 入参校验
├── agent/
│ ├── react_agent.py # ReAct 智能体主逻辑:create_agent、流式执行、熔断防护、历史窗口截断
│ ├── tools/
│ │ ├── agent_tools.py # 7 个业务工具(知识库问答/天气/定位/用户ID/月份/外部数据/报告信号)
│ │ ├── middleware.py # Agent 中间件:工具监控 / 模型前日志 / 报告提示词动态切换
│ │ └── timeout_decorator.py# 工具超时装饰器(全局线程池 + 超时标记)
├── rag/
│ ├── vector_store.py # 向量存储服务:ES(生产) / Chroma(降级)、混合检索、RRF、知识库加载
│ ├── rag_service.py # RAG 业务服务(单例):两阶段检索 + LCEL 生成链路
│ └── rerank_service.py # 精排服务:local(BGE-Reranker) / api(qwen3-rerank) 双模式、重试降级
├── model/
│ └── factory.py # 模型工厂:ChatTongyi / DashScopeEmbeddings(重试配置化)
├── utils/
│ ├── config_handler.py # YAML 配置统一加载
│ ├── session_store.py # Redis 会话存储:LIST 原子追加、旧数据迁移、连接加固
│ ├── logger_handler.py # 日志管理
│ ├── file_handler.py # 文件解析(pdf/txt)与 MD5 工具
│ ├── path_tool.py # 项目路径工具
│ └── prompt_loader.py # 提示词加载
├── eval/
│ ├── eval_runner.py # LLM 自动化评测:用例执行 + 裁判打分 + 汇总指标
│ ├── judge_prompt.txt # 裁判评分提示词
│ ├── testset/agent_testset.json # 评测用例集
│ └── output/ # 评测报告输出目录
├── prompts/
│ ├── main_prompt.txt # 默认系统提示词(知识库问答模式)
│ ├── rag_summarize.txt # RAG 总结提示词(回答必须基于参考资料)
│ └── report_prompt.txt # 报告生成提示词(报告模式动态切换)
├── config/ # YAML 配置:agent / model / vector_store / es / redis / chroma / prompts
├── data/ # 知识库文档(txt/pdf)+ external/records.csv 用户月度数据
├── chroma_db/ # Chroma 持久化目录(降级向量库)
├── logs/ # 运行日志输出目录
├── app_streamlit.py # Streamlit 本地调试界面(可选)
├── requirements.txt # Python 依赖清单
└── docs/ # 技术文档
4. 模块详解
4.1 API 服务层(api/main.py)
- 全局单例 Agent :
agent = ReactAgent(),进程内唯一实例,复用模型与工具注册。 - SSE 流式接口
POST /api/v1/chat/stream:- 入参校验 → 读取历史(单次)→ Agent 流式执行 →
reasoning/final事件 → 原子落库; - 流内异常兜底 :生成器内部逐段 try/except(FastAPI 外层 try 覆盖不到惰性生成器内部),任何异常都保证客户端收到
final+[DONE]; - 历史单次读 :
history_msg参数传入 Agent,消除"API 层与 Agent 内部各读一次 Redis"的双读不一致; - 空答案治理:空回答兜底为友好文案,且不把空回答写入历史。
- 入参校验 → 读取历史(单次)→ Agent 流式执行 →
- reasoning 增量推送 :
stream_mode="values"每帧携带完整消息列表,_extract_reasoning_parts(messages, start_index)只处理新增区间;AI 消息带工具调用时只推「调用工具名 参数=...」摘要,tool 内部返回不进 reasoning,避免刷屏。 - CORS 白名单 :环境变量
ALLOW_ORIGINS(逗号分隔),默认http://localhost:8501;不采用["*"] + credentials组合。 - 入参校验 :query 非空、≤2000 字符;mode 仅
chat/report;非法抛HTTPException(400)。 - 其他接口 :
GET /api/v1/chat/history(按 session_id 取历史)、GET /health(健康检查)。
4.2 Agent 层(agent/react_agent.py)
-
智能体构建 :
create_agent(model=chat_model, system_prompt=..., tools=[7个工具], middleware=[...])高层 API,由 LangGraph 编排 agent 图。 -
流式执行 :
agent.stream(..., stream_mode="values", config={"recursion_limit": max_recursion_limit}),values模式每帧返回完整状态字典{"messages": [...]}。execute_stream(query, session_id):返回文本片段(供 Streamlit/兼容旧接口);execute_stream_with_raw(query, session_id, report, history_msg):yield(state, out_text)二元组,供 API 层做 reasoning 提取;历史优先用调用方传入的history_msg。
-
多层防护 :
防护 参数 机制 工具迭代轮熔断 max_tool_iter=8统计携带 tool_calls 的 AI 消息数 总思考轮熔断 max_total_think_iter=12统计全部新生成 AI 消息数(防纯思考死循环) 原地打转检测 max_dup_tool_call=2同「工具名+参数签名」重复≥2 次熔断( sort_keys=True序列化)图级硬上限 max_recursion_limit = max_total_think_iter*3+10LangGraph 节点级 recursion_limit,消费端 break 后后台仍会执行,此上限兜底终止 生成器显式关闭 finally: gen.close()熔断/异常/正常结束都终止底层流(传递 GeneratorExit) -
历史窗口 :
truncate_conversation_history(history_msg, max_turn=8),一轮 = user+assistant 两条,截断至最近 8 轮。
4.3 工具层(agent/tools/)
| 工具 | 入参 | 说明 |
|---|---|---|
rag_summarize |
query | 两阶段 RAG 知识库问答(rag_summarize_with_rerank) |
get_weather |
city | 天气信息(demo 固定文本) |
get_user_location |
无 | 用户所在城市(demo 随机返回) |
get_user_id |
无 | 默认用户 ID(配置 default_user_id,确定性) |
get_current_month |
无 | 报告统计月份(配置 report_default_month,确定性) |
fetch_external_data |
user_id, month | 读取 records.csv 用户月度数据,返回格式化文本 |
fill_context_for_report |
无 | 报告模式信号:触发中间件切换报告提示词 |
- 超时装饰器
@tool_timeout(timeout_sec=8):所有工具统一 8 秒超时。实现为模块级全局线程池(max_workers=8),超时立即返回{"__tool_timeout__": True, "msg": ...}标记,不等待挂死线程结束;线程池复用避免每次调用新建池的开销。 - 中间件(
middleware.py) :@wrap_tool_call monitor_tool:工具执行监控日志、超时标记转文本、异常转 observation 返回给 LLM(不静默吞错)、fill_context_for_report置runtime.context["report"]=True;@before_model log_before_model:模型调用前日志;@dynamic_prompt report_prompt_switch:根据runtime.context["report"]动态切换 报告/默认 提示词。
4.4 RAG 层(rag/)
VectorStoreService(vector_store.py)
- 双后端:
config/vector_store.yml的vector_backend选择elasticsearch(生产)/chroma。 - ES 初始化:实例化 ElasticsearchStore(字段
vector/text)、动态探测 embedding 维度、IK 分词器(ik_max_word索引 /ik_smart搜索)、recreate=False默认不删已有索引;初始化整体 try/except,失败自动降级 Chroma,不阻断服务启动。 - 混合检索
hybrid_search(query, top_k):纯向量检索top_k*2+ 原生 ES BM25top_k*2→ 手动 RRF 融合(score = Σ 1/(rank+60))→ 截取top_k;运行期异常降级 Chroma 纯向量检索(绝不再次调用已异常的 ES)。 - 知识库加载
load_document():按文件类型解析(txt/pdf)→ 递归字符切分(chunk_size=200, overlap=20)→ 写入当前后端;MD5 去重 (ES 用md5_es.txt、Chroma 用md5.txt,两套记录隔离)。 - Chroma 惰性加载:
@property chroma_db首次访问才创建,ES 后端启动不强制加载。
RagSummarizeService(rag_service.py)
- 单例模式,全局只实例化一次。
- 两阶段检索
retrieve_with_rerank(query):hybrid_search粗召回(top_k=12)→rerank_service.filter精排 → 返回[(text, metadata)](元数据全程透传,供展示引用来源)。 - LCEL 生成链路:
prompt_template | print_prompt | chat_model | StrOutputParser();无匹配资料时上下文置为"知识库中没有找到和该问题相关的参考资料"。 - 提供传统单阶段
rag_summarize(retriever 纯向量)与推荐的两阶段rag_summarize_with_rerank两套接口。
BgeRerankService(rerank_service.py)
- 双模式:
rerank_mode=api(dashscope qwen3-rerank 云端)/local(BAAI/bge-reranker-large 本地模型,modelscope 下载 + transformers 推理,支持 cuda/cpu)。 - 惰性加载:
_ensure_loaded()加锁 + 二次检查,首次filter才加载,避免 import 即阻塞。 - API 重试:瞬时失败按
rerank_api_max_retries重试(日志记录第 n/m 次),重试全部失败才降级(返回占位分数 1.0 的原始文档)。 filter(query, docs_with_meta):打分 → 阈值过滤(≥0.3)→ 截取 top_n=5;本地模型不可用时直接返回粗召回前 top_n。
4.5 模型层(model/factory.py)
ChatModelFactory→ChatTongyi(model=qwen3-max, max_retries=chat_model_max_retries);EmbeddingsFactory→DashScopeEmbeddings(model=text-embedding-v4, max_retries=embedding_model_max_retries);- 全局单例:
chat_model/embed_model模块级实例化一次; - 重试次数全部配置化(
config/model.yml),瞬时网络抖动/限流自动重试,不引入额外组件。
4.6 会话存储层(utils/session_store.py)
- Key 设计 :
chat:msg:{sid}(LIST,元素为 JSON 的 {role, content})、chat:ctx:{sid}(会话上下文,report 标记)。 - 原子追加
append_turn(sid, user_content, assistant_content):pipeline 一次性完成RPUSH(追加 user+assistant 两条)+LTRIM(-200, -1)(限条数)+EXPIRE(7 天 TTL);RPUSH 由 Redis 单线程执行,天然串行化,并发追加不互相覆盖。 - 旧数据兼容 :
get_msg先TYPE判断;string 类型(旧数据)读出后自动迁移为 LIST------先 DELETE 再 RPUSH(真实 Redis 对 string key 执行 RPUSH 报 WRONGTYPE)。 - 连接加固 :
socket_connect_timeout=2/socket_timeout=3/socket_keepalive/health_check_interval=30/Retry(ExponentialBackoff, retries=2);所有方法异常兜底(读取失败返回空列表,不阻断主链路)。
4.7 评测层(eval/eval_runner.py)
- 用例执行:逐条用例调用
agent.execute_stream_with_raw,提取工具调用记录(精简 JSON,避免消息对象序列化超长)。 - LLM 裁判:手工拼接评分 prompt(规避模板渲染 bug),裁判 LLM 输出 JSON(task_success / tool_call_correct / hallucination_exist / answer_score / comment),带代码块提取与解析容错。
- 汇总指标:任务成功率、工具调用准确率、幻觉率、回答平均分;报告输出至
eval/output/eval_report_{ts}.json。 - 健壮性:单条用例异常跳过不中断整体;裁判调用异常返回全失败默认结果。
5. 存储设计
5.1 Redis(会话存储)
| Key | 类型 | 内容 | TTL |
|---|---|---|---|
chat:msg:{sid} |
LIST | 对话消息({role, content} JSON),最多 200 条 FIFO | 7 天 |
chat:ctx:{sid} |
STRING | 会话上下文 JSON(如 {"report": true}) | 7 天 |
写入路径:append_turn(pipeline 原子) / set_context(SETEX);读取路径:get_msg(含旧数据迁移) / get_context(默认 {"report": false});清空:clear_all(sid)。
5.2 Elasticsearch(生产向量库)
- 索引名
rag_knowledge;mapping 字段:text:text 类型,ik_max_word分析器(索引)/ik_smart(搜索),存储文档正文;vector:dense_vector,维度由 embedding 模型动态探测,similarity=cosine;metadata:object 类型,透传文档元数据。
- 检索能力:向量 KNN(
similarity_search(search_type="vector"))+ 原生 BM25(match查询 + ik_max_word)+ RRF 融合。 - 维护命令:
python -m rag.vector_store(加载知识库 / 调试混合检索);reset_knowledge_store()(危险操作:重建索引 + 清空 md5 记录,重新全量导入)。
5.3 Chroma(降级向量库)
collection_name=agent,持久化目录chroma_db/;ES 故障或vector_backend=chroma时使用;- MD5 去重记录独立文件
md5.txt(与 ES 的md5_es.txt隔离)。
6. 配置说明
6.1 config/vector_store.yml
| 配置项 | 默认值 | 说明 |
|---|---|---|
vector_backend |
elasticsearch | 向量后端:elasticsearch / chroma |
k |
5 | retriever 单路返回条数 |
retrieve_top_k |
12 | 两阶段粗召回数量(需 > rerank top_n) |
allow_knowledge_file_type |
txt, pdf | 知识库允许的文件类型 |
chunk_size / chunk_overlap |
200 / 20 | 文档切分参数 |
separators |
句号/问号/感叹号等 | 切分分隔符 |
6.2 config/es.yml
| 配置项 | 默认值 | 说明 |
|---|---|---|
es_host |
http://127.0.0.1:9200 | ES 服务地址 |
es_index_name |
rag_knowledge | 索引名 |
md5_hex_es_store |
md5_es.txt | ES 知识库 MD5 记录文件 |
6.3 config/model.yml
| 配置项 | 默认值 | 说明 |
|---|---|---|
chat_model_name |
qwen3-max | 对话模型 |
embedding_model_name |
text-embedding-v4 | 向量模型 |
rerank_mode |
api | 精排模式:api / local |
rerank_api_model |
qwen3-rerank | 云端精排模型 |
rerank_model_repo / local_model_dir |
BAAI/bge-reranker-large | 本地精排模型 |
rerank_score_threshold |
0.3 | 精排相关性阈值 |
rerank_top_n |
5 | 精排后返回条数 |
chat_model_max_retries |
2 | 对话 LLM 失败重试次数 |
embedding_model_max_retries |
2 | 向量化失败重试次数 |
rerank_api_max_retries |
2 | 云端精排失败重试次数 |
6.4 config/agent.yml
| 配置项 | 默认值 | 说明 |
|---|---|---|
external_data_path |
data/external/records.csv | 用户月度数据文件 |
report_default_month |
2025-12 | 报告统计基准月份(demo 数据集;生产改动态) |
default_user_id |
1001 | 默认用户 ID(demo 固定;生产绑定登录态) |
6.5 config/redis.yml / config/chroma.yml / config/prompts.yml
redis.yml:host / port / password(连接参数在session_store.py另有超时与重试加固);chroma.yml:collection_name=agent、persist_directory=chroma_db、data_path=data、md5_hex_store=md5.txt;prompts.yml:三个提示词文件路径(main / rag_summarize / report)。
6.6 环境变量
| 变量 | 说明 |
|---|---|
DASHSCOPE_API_KEY |
阿里云百炼 API Key(对话/向量/精排共用) |
ALLOW_ORIGINS |
CORS 白名单(逗号分隔),默认 http://localhost:8501 |
7. 部署运维
7.1 环境依赖
- Python 3.10+(本项目实测 3.12.10)
- 外部服务:Redis、Elasticsearch(8.x,需安装 IK 分词插件)、Chroma(可选,自动降级用)
- 依赖安装:
pip install -r requirements.txt -i https://pypi.tuna.tsinghua.edu.cn/simple - 模型访问:配置
DASHSCOPE_API_KEY(对话/向量/精排全走云端,无需本地模型;local 精排模式才需要下载 BGE 模型)
7.2 启动步骤
bash
# 1. 确保 Redis / ES 已启动
# 2. 首次使用:导入知识库(ES 后端)
python -m rag.vector_store
# 3. 启动后端服务
python -m api.main # uvicorn api.main:app --host 0.0.0.0 --port 8000
# 4.(可选)启动 Streamlit 本地调试界面
streamlit run app_streamlit.py
7.3 健康检查与验证
bash
curl http://localhost:8000/health # {"code":200,"msg":"service running"}
curl -N -X POST http://localhost:8000/api/v1/chat/stream \
-H "Content-Type: application/json" \
-d '{"session_id":"test1","query":"充电底座指示灯闪烁是什么原因?","mode":"chat"}'
# 返回 SSE:data:{"type":"reasoning",...} → data:{"type":"final",...} → data:[DONE]
curl "http://localhost:8000/api/v1/chat/history?session_id=test1" # 查看已落库历史
7.4 日志与故障排查
日志统一走 utils/logger_handler.py,输出至 logs/ 目录。关键日志关键字速查:
| 场景 | 日志关键字 |
|---|---|
| ES 初始化失败降级 | [ES初始化]ES 不可用,自动降级使用 Chroma 向量检索 |
| 混合检索降级 | [hybrid_search]降级执行Chroma纯向量检索 |
| Rerank 降级 | rerank api重试...仍失败,执行降级 |
| 工具执行 | [tool monitor] 执行工具:{name} / 传入参数 / 返回结果 |
| 工具超时 | [tool monitor] 【工具执行超时】 |
| 熔断 | ⚠️智能体触发熔断 / ⚠️检测到原地打转 |
| Agent 异常 | [ReactAgent 顶层运行异常] |
| SSE 兜底 | [SSE]Agent 流式执行异常 / [SSE]历史落库失败 |
| Redis 异常 | [RedisSession.*] |
| 知识库加载 | [加载知识库] / [ES初始化]索引...已存在 |
常见问题速查:
| 现象 | 排查方向 |
|---|---|
| 流中断无提示 | 查 [SSE]Agent 流式执行异常 / [ReactAgent 顶层运行异常];确认 Nginx 未缓冲 event-stream |
| ES 挂了服务是否可用 | 服务不崩,自动降级 Chroma;查 [ES初始化] / [hybrid_search] 日志确认降级生效 |
| 并发丢消息 | Redis 已改 LIST 原子追加;确认新代码走 append_turn 而非 save_msg 覆盖写 |
| 报告内容漂移 | 确认 get_user_id / get_current_month 走配置值(确定性) |
| 回答与历史对不上 | 确认 API 层单次读历史并传 history_msg(无双读) |
| 工具卡死不返回 | 查 tool_timeout 超时标记日志;挂死线程受全局池 max_workers=8 约束 |
8. API 参考
8.1 POST /api/v1/chat/stream
SSE 流式对话接口。
请求体(JSON):
| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
session_id |
string | 否 | 会话 ID;不传则服务端生成 uuid |
query |
string | 是 | 用户问题,strip 后非空且 ≤2000 字符 |
mode |
string | 否 | chat(默认)/ report |
响应 :Content-Type: text/event-stream,事件序列:
data:{"type":"reasoning","content":"🔍 Agent思考过程: \n..."}
data:{"type":"final","content":"最终回答..."}
data:[DONE]
| 事件 | 说明 |
|---|---|
reasoning |
思考过程增量(含工具调用摘要「调用工具名 参数=...」) |
final |
最终回答(空答案有兜底文案;异常时有"服务暂时异常"提示) |
[DONE] |
流结束标记 |
错误码:400(入参非法:query 为空/超长、mode 非法)。
8.2 GET /api/v1/chat/history?session_id=xxx
返回 {"history": [{"role":"user","content":"..."}, {"role":"assistant","content":"..."}]}。
8.3 GET /health
返回 {"code":200,"msg":"service running"},供负载均衡/探活使用。
9. 扩展指南
9.1 新增一个业务工具
- 在
agent/tools/agent_tools.py定义工具(@tool(description=...)+@tool_timeout(timeout_sec=8)),返回字符串或超时标记 dict; - 在
agent/react_agent.py的create_agent(tools=[...])列表中加入该工具; - 如需特殊行为(如报告信号、结果转换),在
middleware.py的monitor_tool中按tool_name分支处理; - 在评测用例集
eval/testset/agent_testset.json补充用例(expect_tool_list指向新工具)。
9.2 新增知识库文档
- 将 txt/pdf 放入
data/目录; - 运行
python -m rag.vector_store:MD5 去重自动跳过已入库文件,新文件切分后写入当前后端(ES/Chroma); - 若需全量重建 ES 知识库:调用
reset_knowledge_store()(会清空索引,谨慎操作)。
9.3 切换模型 / 精排模式
- 对话/向量模型:改
config/model.yml的chat_model_name/embedding_model_name(及*_max_retries); - 精排:
rerank_mode: api|local;api 需DASHSCOPE_API_KEY,local 需下载 BAAI/bge-reranker-large(自动经 modelscope 下载至local_model_dir); - 重试与阈值调参:
rerank_api_max_retries/rerank_score_threshold/rerank_top_n。
9.4 新增 HTTP 接口
在 api/main.py 追加 FastAPI 路由;流式接口请遵循"生成器内部逐段 try/except + 结尾 DONE"范式,保证异常可见、流不悬挂。
9.5 新增评测用例
在 eval/testset/agent_testset.json 添加 {case_id, case_type(qa/report), user_query, expect_tool_list, expect_key_points, forbidden_points};运行 python -m eval.eval_runner,报告输出到 eval/output/。
9.6 前端接入
- Streamlit:
app_streamlit.py(本地调试); - 第三方系统:直接调用
/api/v1/chat/stream,按reasoning/final/[DONE]事件渲染;跨域需在服务端ALLOW_ORIGINS加入前端域名。