智净灵境 — 基于 LangChain RAG + MCP 的扫地/拖地机器人 AI 智能客服系统。 采用 Streamlit 构建双页面 Web 应用(用户智能问答 + 商家知识库管理),以 Qwen3-Max 作为推理引擎,ChromaDB 作为向量数据库实现语义检索,通过 MCP 协议集成高德地图实现 IP 定位与天气感知。
概览
是一个面向扫地/拖地机器人领域的 AI 智能客服系统,提供双页面 Web 应用: - 语义检索:基于向量相似度的知识库搜索,支持维护保养、故障排除、选购指南、常见问题 - 工具调用:LLM 自主决策何时查知识库、何时调高德地图 API 获取位置/天气 - 会话持久化:对话历史自动保存到本地 JSON 文件,刷新不丢失 - 去重入库:基于 MD5 的内容去重,避免重复文档片段污染检索结果 以下解析遵循 ,每个关键决策都包含:所用技术、选择原因、底层原理、替代方案对比、优越性分析、知识迁移路径。 📌 设计 Streamlit + LangChain Agent + RAG + MCP 混合智能客服系统架构 - LLM 训练数据有截止日期,无法覆盖最新的扫地机器人型号和故障码 - 用户问题可能涉及实时信息(天气影响清洁建议、根据地区推荐购买渠道) - 需要单一入口处理多种意图(产品咨询 / 故障排查 / 天气关联 / 购买建议) - 团队规模小,无法为每种意图单独开发路由逻辑 - Agent 模式让 LLM 自主选择调用哪个工具,无需人工编写意图分类器,天然覆盖长尾问题 - 工具可以是本地检索器(RAG)也可以是远程 API(MCP),统一接口,扩展方便 - LangChain 的 create_agent() 内置了 ReAct 循环(思考→行动→观察→再思考),处理多步推理 LangChain create_agent() 将这个循环内置在 agent.ainvoke() 中,开发者无需手写循环逻辑。LLM 的每一次工具调用都是一次完整的 API 请求,所以 Agent 模式的 token 消耗 = 普通对话的 N 倍(N = 工具调用次数 + 1)。 工具是 LLM 的"手"。每个工具包含三个要素:名称(name)、描述(description,告诉 LLM 何时用)、输入 schema(parameters)。LLM 不执行工具,而是输出一个 ToolCall 消息;LangChain 框架拦截这个消息,执行对应函数,然后将结果以 ToolMessage 形式注入对话上下文。
README
🤖 智净灵境 — 扫地/拖地机器人 AI 智能客服系统
Powered by LangChain RAG + MCP + ChromaDB + 阿里百炼 DashScope
📋 目录
1. 项目概述
智净灵境 是一个面向扫地/拖地机器人领域的 AI 智能客服系统,提供双页面 Web 应用:
| 页面 | 用户 | 功能 |
|---|---|---|
| 💬 智能客服 | 终端消费者 | 自然语言问答、地理位置感知、天气关联建议 |
| 📚 知识库管理 | 商家/运营 | 上传文档、查看入库状态、全量重建知识库 |
核心能力:
- 语义检索:基于向量相似度的知识库搜索,支持维护保养、故障排除、选购指南、常见问题
- 工具调用:LLM 自主决策何时查知识库、何时调高德地图 API 获取位置/天气
- 会话持久化:对话历史自动保存到本地 JSON 文件,刷新不丢失
- 去重入库:基于 MD5 的内容去重,避免重复文档片段污染检索结果
2. 技术架构深度解析
以下解析遵循 deep-teach 深度教学模式,每个关键决策都包含:所用技术、选择原因、底层原理、替代方案对比、优越性分析、知识迁移路径。
🎯 STEP 1 整体架构设计
📌 操作: 设计 Streamlit + LangChain Agent + RAG + MCP 混合智能客服系统架构
① 🛠️ 所用技术
| 属性 | 内容 |
|---|---|
| 架构模式 | Agent-Tool 混合架构(LLM 作为路由器,动态选择工具) |
| 分类 | AI 应用架构 / 检索增强生成 / 智能体模式 |
| 角色 | 将 LLM 作为中央决策引擎,通过工具调用机制编排知识库检索与外部 API |
② 💡 为什么选择 Agent-Tool 混合架构
项目约束:
- LLM 训练数据有截止日期,无法覆盖最新的扫地机器人型号和故障码
- 用户问题可能涉及实时信息(天气影响清洁建议、根据地区推荐购买渠道)
- 需要单一入口处理多种意图(产品咨询 / 故障排查 / 天气关联 / 购买建议)
- 团队规模小,无法为每种意图单独开发路由逻辑
匹配原因:
- Agent 模式让 LLM 自主选择调用哪个工具,无需人工编写意图分类器,天然覆盖长尾问题
- 工具可以是本地检索器(RAG)也可以是远程 API(MCP),统一接口,扩展方便
- LangChain 的
create_agent()内置了 ReAct 循环(思考→行动→观察→再思考),处理多步推理
Trade-off:
✅ 选择了:LLM 自主决策的灵活性,无需硬编码路由规则
❌ 放弃了:确定性的执行路径,增加了 token 消耗和延迟
⚖️ 这个交换在本项目中是值得的,因为:客服场景的意图多样性远超规则能覆盖的范围,
且每次对话多消耗的 token 成本远低于人工客服的成本
③ 📚 技术深度剖析
【核心原理】Agent-Tool 循环(ReAct 模式)
Agent 的核心是 ReAct(Reasoning + Acting)循环,它本质上是一个 while 循环:
循环开始
│
├─→ LLM 分析当前状态:我已知什么?用户问了什么?
│
├─→ LLM 决策:是否需要调用工具?
│ ├─ 是 → 输出 ToolCall(工具名 + 参数)
│ │ → 执行工具 → 观察结果 → 回到循环开头
│ └─ 否 → 输出最终文本回复
│
└─→ 循环结束
LangChain create_agent() 将这个循环内置在 agent.ainvoke() 中,开发者无需手写循环逻辑。LLM 的每一次工具调用都是一次完整的 API 请求,所以 Agent 模式的 token 消耗 = 普通对话的 N 倍(N = 工具调用次数 + 1)。
【关键概念】
概念 1:Tool(工具) 工具是 LLM 的"手"。每个工具包含三个要素:名称(name)、描述(description,告诉 LLM 何时用)、输入 schema(parameters)。LLM 不执行工具,而是输出一个 ToolCall 消息;LangChain 框架拦截这个消息,执行对应函数,然后将结果以 ToolMessage 形式注入对话上下文。
# 工具定义示例(来自 rag/agent.py:74-89)
@tool # ← 装饰器自动提取函数名和文档字符串作为 name/description
def search_knowledge_base(query: str) -> str:
"""搜索扫地机器人知识库。涵盖范围:产品维护保养、故障排除、选购指南、常见问题解答。
:param query: 搜索关键词或问题,例如"日常如何保养"、"E13错误代码"、"适合宠物家庭的机型"
""" # ← 这段 docstring 是 LLM 判断何时调用此工具的唯一依据
docs = retriever.invoke(query) # ← 实际执行:调用 ChromaDB 向量检索
if not docs:
return "[知识库] 未找到相关参考资料" # ← 明确的失败反馈,LLM 据此调整策略
parts = []
for doc in docs:
source = doc.metadata.get("source", "未知来源")
parts.append(f"[来源: {source}]\n{doc.page_content}") # ← 标注来源,可追溯
return "\n\n---\n\n".join(parts)
概念 2:System Prompt(系统提示词) System Prompt 是 LLM 的"宪法",定义了它的角色、可用工具、行为规则。在本项目中,System Prompt 被动态注入了用户 IP,使 LLM 在调用位置工具时可以直接使用:
# 动态 System Prompt(来自 rag/agent.py:56-62)
ip_hint = (
f"当前用户的公网IP地址是: {user_ip}。"
f"当用户要求查询位置或天气时,请直接使用此IP调用 maps_ip_location。"
if user_ip
else ""
)
system_prompt = SYSTEM_PROMPT.format(ip_hint=ip_hint)
概念 3:RAG(检索增强生成) RAG 是"给 LLM 外挂一个搜索引擎"。流程分两阶段:
离线阶段(建库):
文档 → 分割成chunks → 向量化(Embedding) → 存入向量数据库
在线阶段(问答):
用户问题 → 向量化 → 相似度检索 → 取 Top-K chunks → 拼入 LLM 上下文 → LLM 生成回答
⚠️ 常见陷阱
| # | 陷阱 | 后果 | 避免/解决方法 |
|---|---|---|---|
| 1 | 工具描述写得太模糊 | LLM 不知道何时调用该工具,或错误调用 | 描述要具体到触发场景和参数含义,如本例中明确了"日常保养""E13错误代码"等具体示例 |
| 2 | 检索结果为空时不返回明确信号 | LLM 可能编造(幻觉)答案 | 必须返回 [知识库] 未找到相关参考资料,让 LLM 诚实告知用户 |
| 3 | Agent 没有限制工具调用次数 | 无限循环消耗大量 token | LangChain 内置了递归限制(默认约 25 步),且 Agent 会自己判断何时停止 |
④ 🔄 可替代方案对比
| 对比维度 | ✅ Agent-Tool(本选) | 🔶 意图分类路由 | 🔶 纯 RAG 无 Agent |
|---|---|---|---|
| 核心技术 | LLM 作为路由器 | 训练分类模型 + if-else | 检索后直接拼接 Prompt |
| 核心优势 | 零规则维护,自动覆盖长尾 | 低延迟,确定性高 | 实现最简单,无额外 API 调用 |
| 主要劣势 | 多次 LLM 调用,延迟高 | 需要标注数据,长尾覆盖差 | 无法调用外部 API,无法多步推理 |
| 适用场景 | 意图多样、需要外部工具 | 意图固定、对延迟敏感 | 纯文档问答,不需要外部数据 |
| 性能表现 | 2-5 次 LLM 调用/问 | 1 次 LLM 调用/问 | 1 次 LLM 调用/问 |
| 学习曲线 | 中等 | 陡峭(需要 ML 标注) | 平缓 |
对比总结: 本项目选择 Agent-Tool 架构是因为扫地机器人客服的意图高度分散(保养/故障/选购/天气关联),且需要整合异构数据源(文档库 + 地图 API)。意图分类路由无法覆盖长尾,纯 RAG 无法访问外部实时数据。Agent-Tool 虽然增加了延迟(2-5 次 API 调用),但每次调用的 token 开销与实际客服成本相比可忽略。
⑤ ⭐ 本选技术的优越性
1. 零规则维护
- 说明:无需编写任何
if "保养" in question: call_baoyang()的硬编码路由。加一个新知识领域只需放入新文档,Agent 自动知道何时检索。加一个新外部工具只需注册到 Agent,LLM 会自动学习何时调用。 - 数据支撑:本项目的 5 个 TXT 文件覆盖了 5 个完全不同的知识领域(维护、选购、故障、FAQ、100 问),但 System Prompt 没有任何领域特定的分支逻辑。
2. 可扩展的工具生态
- 说明:MCP 协议提供了标准化的工具接入方式。当前接入高德地图(位置+天气),未来可接入物流查询、价格比较、售后工单等,Agent 自动学会编排多工具协作。
- 数据支撑:
McpClientManager.get_tools_for(domain)的领域设计(location/weather/poi)预留了扩展点,添加新领域只需在tool_domains字典新增一行。
3. 天然的多步推理
- 说明:用户问"今天下雨,我的扫地机器人要怎么保养?“时,Agent 自动规划:先查天气 → 得知下雨 → 再查知识库"潮湿环境保养” → 综合给出建议。这个过程无需任何人工编排。
- 数据支撑:System Prompt 中明确写了"当用户问题涉及’根据天气’、'这里的气候’等需要结合环境的场景,先查天气和位置,再查知识库给建议"。
📊 生态成熟度评估:
| 指标 | 数据 | 评级 |
|---|---|---|
| LangChain GitHub Stars | 100k+ | ⭐⭐⭐⭐⭐ |
| LangChain 周下载量 | 500 万+ | 高 |
| 文档质量 | 8/10(丰富但版本碎片化) | 良好 |
| 维护频率 | 每天多次 PR 合并 | 活跃 |
| 生产验证 | 数千家企业使用 | 广泛验证 |
| 社区支持 | Discord + GitHub + 中文社区 | 响应快 |
⑥ 🔗 知识延伸与迁移
🔄 思想迁移 — Agent-Tool 模式的本质思想还能用在:
- 场景 1:代码助手(Copilot) — LLM 作为 Agent,工具集 = 文件搜索 + 终端执行 + Git 操作 + Linter
- 场景 2:电商导购 — LLM 作为 Agent,工具集 = 商品搜索 + 价格比较 + 库存查询 + 用户画像
- 场景 3:运维助手 — LLM 作为 Agent,工具集 = 日志查询 + K8s 操作 + 告警查询 + Runbook 检索
📖 完整学习路径:
前置知识: [Python asyncio, Prompt Engineering 基础]
↓
【Agent-Tool 架构】
↓
进阶方向: [Multi-Agent 协作, LangGraph 状态图, Agent 可观测性, 安全沙箱]
🎯 STEP 2 RAG 检索增强生成
📌 操作: 实现文档加载 → 文本分割 → 向量化 → 去重入库 → 语义检索的完整 RAG 管线
① 🛠️ 所用技术
| 属性 | 内容 |
|---|---|
| 技术 | LangChain RAG 管线 + RecursiveCharacterTextSplitter + DashScope text-embedding-v4 |
| 分类 | 检索增强生成 / 向量检索 / 文档处理管线 |
| 角色 | 将非结构化中文文档转化为可语义搜索的向量化知识库 |
② 💡 为什么选择 RecursiveCharacterTextSplitter + DashScope Embedding
项目约束:
- 文档为中文,分隔符和英文不同(句号是"。“不是”.")
- 文档长度不一(短的几百字,长的几万字),需要自适应分割
- Embedding 需要处理中文语义,不能依赖英文模型
- 需要在本地持久化,避免每次重启都重新计算向量
匹配原因:
RecursiveCharacterTextSplitter按优先级列表递归分割:先按段落\n\n,再按句子。!?,最后按字符,保证中文语义单元不被截断text-embedding-v4是阿里百炼的中文优化 Embedding 模型,在中文语义相似度基准上优于 OpenAI text-embedding-3-small- ChromaDB 内置持久化到磁盘,无需额外配置
Trade-off:
✅ 选择了:中文优化的分隔符 + 国产 Embedding 模型
❌ 放弃了:更通用的英文分隔符 + OpenAI 模型(需要外网访问)
⚖️ 值得是因为:本项目 100% 中文内容,中文 Embedding 模型在语义检索准确率上优势明显
③ 📚 技术深度剖析
【核心原理】文本分割的递归算法
RecursiveCharacterTextSplitter 不是简单的按字符数切分,而是按语义层级递归尝试:
给定一段文本,目标 chunk_size=500:
1. 尝试用 "\n\n" 分割 → 如果某段仍 > 500 → 对该段尝试 "\n"
2. 尝试用 "\n" 分割 → 如果某段仍 > 500 → 对该段尝试 "。" / "."
3. 尝试按句子分割 → 如果某句仍 > 500 → 按 " " 分割
4. 最后按字符硬切 → 保证最终每段 ≤ 500
本项目自定义了中文分隔符列表(rag/config.py:29-31):
separators: list = field(default_factory=lambda: [
"\n\n", "\n", ".", "!", "?", "。", "!", "?", " ", ""
])
# ↑英文 ↑中文标点 ↑字符级兜底
关键设计: 小于 max_split_threshold(500 字符)的短文本直接保留完整,不做分割。这保证了类似"E13 错误代码怎么办"这样已经自成一体的短 FAQ 不会被切碎。
【关键概念】
概念 1:Embedding(向量嵌入)
Embedding 是将文本映射到高维空间中的坐标点。语义相近的文本,其向量在空间中距离也近。
"扫地机器人怎么保养" → [0.023, -0.451, 0.789, ..., 0.112] ← 1536 维向量
"清洁机器人日常维护" → [0.019, -0.447, 0.793, ..., 0.108] ← 向量距离很近(语义相似)
"今天天气怎么样" → [0.891, 0.234, -0.567, ..., -0.432] ← 向量距离很远(语义不相关)
ChromaDB 使用余弦相似度计算查询向量与库中所有向量的距离,返回最接近的 Top-K 个文档片段。
概念 2:Chunk Overlap(片段重叠)
chunk_size: int = 500 # 每段 500 字符
chunk_overlap: int = 50 # 相邻段之间重叠 50 字符
重叠的目的:
chunk_1: "...清洁机器人需要定期[更换滤网和边刷]。建议..."
chunk_2: "[更换滤网和边刷]。建议每 3 个月..."
如果不重叠,[更换滤网和边刷] 恰好落在 chunk_1 末尾和 chunk_2 开头,语义被割裂。50 字符的重叠让关键信息在相邻 chunk 之间形成"桥梁",保证检索时至少有一个 chunk 包含完整的语义片段。
【实现细节】
- 数据结构:
DedupService._records使用 Pythonset,O(1) 平均查找时间 - 算法复杂度: MD5 去重 O(n) / 向量检索 O(log n)(HNSW 近似最近邻)
- 关键机制:
DedupService.mark()采用 append-only 写入,不重写整个文件,避免大规模去重时的 IO 瓶颈 - 执行流程: 文件 → DocumentLoader.load() → DedupService.exists() → TextSplitter.split() → vector_store.add_texts() → DedupService.mark()
⚠️ 常见陷阱
| # | 陷阱 | 后果 | 避免/解决方法 |
|---|---|---|---|
| 1 | Embedding 模型与 Retrieval 阶段不一致 | 检索结果与查询不相关 | query_instruction 和 text_instruction 参数需正确设置(检索模型通常区分查询和文档两种模式) |
| 2 | chunk_overlap 设为 0 | 语义被切分在边界处,检索召回率下降 | 本项目设置 overlap=50(chunk_size 的 10%),是业界经验值 |
| 3 | 不设 max_split_threshold | 短文本(如单个 FAQ)被强制切碎,破坏语义完整性 | 本项目设置 500 字符阈值,短文本原样保留 |
④ 🔄 可替代方案对比
| 对比维度 | ✅ ChromaDB(本选) | 🔶 FAISS | 🔶 Milvus |
|---|---|---|---|
| 核心优势 | 零配置持久化,Python 原生 | 极致速度,纯内存 | 分布式、十亿级数据 |
| 主要劣势 | 单机限制,不可横向扩展 | 持久化需手动管理 | 部署运维成本高 |
| 适用场景 | 单机中小规模(<100 万向量) | 离线批量检索 | 企业级大规模生产 |
| 性能表现 | HNSW 索引,<10ms/查询 | IVF 索引,<5ms/查询 | 分布式,<100ms/查询 |
| 学习曲线 | 平缓(pip install 即用) | 中等 | 陡峭(需要 K8s) |
| 生态成熟度 | 高 | 高(Meta 维护) | 高(Zilliz 维护) |
对比总结: 本项目知识库规模预计不超过 1 万条文档片段,ChromaDB 的单机架构完全够用。FAISS 的纯内存模式重启即丢失数据,需要手动序列化;Milvus 的运维负担对一个小型客服系统来说是过度设计。ChromaDB 的"pip install + 自动持久化"是最符合项目约束的选择。
⑤ ⭐ 本选技术的优越性
1. 零运维持久化
- 说明:ChromaDB 将向量数据和元数据存储在本地
./rag/chroma_db/目录,重启后自动加载,不需要启动任何独立服务进程。 - 数据支撑:项目中
persist_directory配置就是唯一的持久化设置,代码中无需任何load()或restore()调用。
2. 与 LangChain 深度集成
- 说明:ChromaDB 实现了 LangChain 的
VectorStore接口,返回的as_retriever()可以直接传入 LangChain Chain 或 Agent。 - 数据支撑:
knowledge_base.py:137-139中self.vector_store.as_retriever(search_kwargs={"k": self.config.retrieval_top_k})一行代码即完成检索器创建。
3. 内建元数据支持
- 说明:每次入库时附带
source和create_time元数据,检索结果可以追踪到原始文件,可追溯性远超纯向量检索。 - 数据支撑:检索结果中包含
[来源: 维护保养.txt]标记,用户在收到回答时可以知道信息来自哪个文档。
🎯 STEP 3 LangChain Agent 智能体
📌 操作: 使用 create_agent() 构建 LLM 驱动的工具调用智能体 + 异步对话管理 + 会话历史持久化
① 🛠️ 所用技术
| 属性 | 内容 |
|---|---|
| 技术 | LangChain create_agent() + ChatTongyi (Qwen3-Max) + 自定义 Tool |
| 分类 | AI Agent 框架 / LLM 工具调用 / 会话管理 |
| 角色 | 将 LLM、RAG 检索器、MCP 工具组装为可自主决策的对话智能体 |
② 💡 为什么选择 LangChain create_agent() + Qwen3-Max
项目约束:
- 需要一个支持工具调用(Function Calling)的国产大模型(网络限制)
- Agent 需要管理多轮对话历史并持久化
- 需要异步执行以支持 Streamlit 的 async 桥接
匹配原因:
ChatTongyi是阿里百炼的 LangChain 集成,直接对接 Qwen 系列模型。Qwen3-Max 原生支持 Function Calling,是目前国产模型中 Agent 能力最强的之一create_agent()封装了 ReAct 循环、工具调用解析、错误重试,开发者只需提供 tools 列表和 system_promptBaseChatMessageHistory接口 +FileChatHistory自定义实现提供了灵活的持久化方案
Trade-off:
✅ 选择了:LangChain 高层封装 + Qwen3-Max + 本地 JSON 文件存储
❌ 放弃了:更底层的手动 ReAct 循环(可控性)+ 数据库存储(并发能力)
⚖️ 值得是因为:项目当前是单用户单机环境,本地 JSON 的简单性远胜数据库的运维成本
③ 📚 技术深度剖析
【核心原理】create_agent() 内部机制
create_agent() 并不是什么魔法,它内部做了三件事:
- 绑定工具到 LLM: 将 Python 函数转为 LLM 能理解的 JSON Schema,通过
model.bind_tools()注入到每次 API 调用的参数中 - 创建 Agent 图(Graph): 构建一个循环执行的 LangGraph 图结构(LLM 节点 → 工具执行节点 → LLM 节点 → …)
- 解析 ToolCall: 拦截 LLM 返回的 tool_call 消息,匹配本地函数,执行后将结果以 ToolMessage 格式反馈给 LLM
# create_agent() 等价于手动编写的以下流程(简化版)
while True:
response = llm.invoke(messages) # 1. LLM 决策
if not response.tool_calls: # 2. 没有工具调用 → 结束
return response
for tool_call in response.tool_calls: # 3. 执行每个工具
tool_result = execute_tool(tool_call.name, tool_call.args)
messages.append(ToolMessage(content=tool_result)) # 4. 结果注入上下文
# 5. 回到循环开头,LLM 根据工具结果继续推理
【关键概念】
概念 1:异步对话管理 + 历史持久化
本项目实现了完整的会话生命周期管理:
# rag/agent.py:92-107 — 一次完整的问答流程
async def query(self, question: str, session_id: str = "default") -> str:
history = self.history_manager.get(session_id) # ① 按 session 加载历史
messages = history.messages + [HumanMessage(content=question)] # ② 拼接历史 + 新问题
result = await self.agent.ainvoke({"messages": messages}) # ③ Agent 执行
all_messages = result["messages"]
history.clear() # ④ 清空旧历史
history.add_messages(all_messages) # ⑤ 写入新历史(含工具调用中间结果)
return all_messages[-1].content # ⑥ 返回最后一条 AI 消息
概念 2:Streamlit Async 桥接
Streamlit 是同步框架,但 LangChain Agent 和 MCP 客户端都是异步的。run_async() 函数是它们之间的桥梁:
# app.py:79-89
def run_async(coro):
"""在同步上下文中安全执行异步协程"""
try:
loop = asyncio.get_running_loop() # 检测是否已在事件循环中
except RuntimeError:
return asyncio.run(coro) # 没有循环 → 新建一个执行
new_loop = asyncio.new_event_loop() # 已有循环(如嵌套调用)→ 新建独立循环
try:
return new_loop.run_until_complete(coro)
finally:
new_loop.close() # 确保清理,避免资源泄漏
概念 3:Session 隔离
每个用户 IP 对应独立的 Agent 实例和会话历史,数据完全隔离:
# app.py:131-143
def get_or_create_agent():
user_ip = st.session_state.get("user_ip", "")
cache_key = f"_agent_{user_ip}" # IP 作为隔离键
if cache_key not in st.session_state:
st.session_state[cache_key] = RagAgent(
config=config, mcp_tools=mcp_tools, user_ip=user_ip
) # 首次访问 → 创建新 Agent(含新聊天历史)
return st.session_state[cache_key]
⚠️ 常见陷阱
| # | 陷阱 | 后果 | 避免/解决方法 |
|---|---|---|---|
| 1 | Agent 历史不清理导致无限增长 | 每次请求携带全部历史,token 消耗暴涨 | history.clear() + history.add_messages(all_messages) 确保每次只保留上一轮完整历史 |
| 2 | async 在 Streamlit 中不当使用 | RuntimeError: This event loop is already running | 本项目 run_async() 通过 try/except 检测运行循环并创建新 loop |
| 3 | session_id 使用固定值 | 多用户共享同一会话历史 | 本项目使用 uuid.uuid4() 生成唯一 session_id |
④ 🔄 可替代方案对比
| 对比维度 | ✅ LangChain create_agent() | 🔶 手写 ReAct 循环 | 🔶 LangGraph 自定义图 |
|---|---|---|---|
| 核心优势 | 一行代码创建 Agent | 最大可控性 | 灵活的状态图(多路分支、人工审批) |
| 主要劣势 | 黑盒,难以调试中间状态 | 代码量大,易出错 | 学习曲线陡峭 |
| 适用场景 | 标准 Agent 场景(本项目的场景) | 需要精细控制每一步 | 复杂多步骤工作流 |
| 性能表现 | 框架开销 < 100ms | 零框架开销 | 框架开销 < 200ms |
| 学习曲线 | 平缓 | 陡峭(需要理解所有细节) | 中等偏陡 |
对比总结: 本项目的 Agent 需求是标准的:搜索知识库 + 调用外部 API。create_agent() 完美覆盖这个场景,不需要 LangGraph 的复杂状态管理,也不需要承受手写循环的维护成本。
🎯 STEP 4 MCP 模型上下文协议集成
📌 操作: 通过 MCP 协议接入阿里百炼高德地图服务,提供 IP 定位和天气查询工具
① 🛠️ 所用技术
| 属性 | 内容 |
|---|---|
| 技术 | MCP (Model Context Protocol) + 阿里百炼高德地图 MCP Server + 单例模式 |
| 分类 | 协议集成 / 工具接入 / 设计模式 |
| 角色 | 通过标准化协议将第三方 API 封装为 LLM 可调用的工具 |
② 💡 为什么选择 MCP 协议 + 单例模式
项目约束:
- 需要接入高德地图 API(位置 + 天气),但不想维护 API 调用的样板代码
- 多个页面和组件可能用到相同的 MCP 工具,需避免重复连接
- 网络请求有延迟和失败风险,需要合理的缓存策略
匹配原因:
- MCP 是 Anthropic 提出的标准化协议,阿里百炼已托管了高德地图 MCP Server,一行 URL 配置即可获取
maps_ip_location、maps_weather等工具,省去数百行 API 封装代码 - 单例模式(
__new__+_instance)保证整个应用只有一个 MCP 连接和一份工具缓存 _tools_cache字典按领域(location/weather/poi)缓存工具列表,避免每次请求都去拉取远程工具定义
Trade-off:
✅ 选择了:MCP 托管服务(零维护 API 封装)+ 客户端单例
❌ 放弃了:直接调高德 API(更灵活参数)+ 工具级别的动态发现(每次拉取最新工具定义)
⚖️ 值得是因为:扫地机器人客服不需要高德 API 的全部功能,MCP 暴露的定位+天气已覆盖需求
③ 📚 技术深度剖析
【核心原理】MCP 协议交互流程
MCP 是客户端-服务器架构:
┌──────────────┐ ┌─────────────────────┐ ┌──────────────┐
│ LangChain │ HTTP │ 阿里百炼 MCP │ 内部 │ 高德地图 │
│ Agent │ ←─────→ │ Server (托管) │ ←─────→ │ API 服务 │
│ (客户端) │ JSON │ │ API │ │
└──────────────┘ └─────────────────────┘ └──────────────┘
步骤:
1. 客户端连接 MCP Server → 获取工具列表(tools/list)
2. Agent 决定调用某工具 → 客户端发送 tools/call(工具名 + 参数)
3. MCP Server 内部调用高德 API → 返回结果
4. 结果返回给 Agent → Agent 基于结果生成回复
【关键概念】
概念 1:单例模式(Singleton)
单例模式的核心思想:一个类在全局永远只有一个实例。实现方式是通过拦截 __new__(Python 中对象的创建方法):
# mcp_service/mcp_client.py:29-42
_instance: "McpClientManager | None" = None # ← 类变量,全局唯一
def __new__(cls) -> "McpClientManager":
if cls._instance is None: # ① 还没实例化过?
cls._instance = super().__new__(cls) # ② 创建唯一实例
cls._instance._initialized = False
return cls._instance # ③ 返回同一个实例(无论调用多少次)
def __init__(self):
if self._initialized: # ④ __init__ 会被多次调用
return # ⑤ 但只有第一次执行初始化逻辑
self._client = None
self._tools_cache = {} # ⑥ 缓存只会创建一次
self._initialized = True
生活类比:单例模式就像一家公司只有一个 CEO。无论你从哪个部门(页面/组件)找 CEO,找到的都是同一个人。
概念 2:懒加载(Lazy Loading)+ 缓存
# mcp_service/mcp_client.py:46-58
async def _get_client(self) -> MultiServerMCPClient:
if self._client is None: # ← 第一次调用才创建连接
self._client = MultiServerMCPClient({
"amap-server": {
"transport": CONFIG.mcp_transport, # "http"
"url": CONFIG.mcp_url, # 高德 MCP 服务地址
"headers": {
"Authorization": f"Bearer {CONFIG.api_key}"
}
}
})
return self._client # ← 后续调用直接返回缓存的客户端
懒加载的优势:如果用户只访问知识库管理页面(不需要位置天气),MCP 连接根本不会创建,节省资源。
⚠️ 常见陷阱
| # | 陷阱 | 后果 | 避免/解决方法 |
|---|---|---|---|
| 1 | 单例在多线程环境下未加锁 | T1 和 T2 同时检测到 _instance is None,创建两个实例 |
Python GIL 在创建实例时有天然保护;严格场景可用 threading.Lock 包裹 |
| 2 | 缓存不过期 | MCP Server 升级了新工具,但客户端仍用旧缓存 | 本项目的工具集合相对固定;如果需要动态刷新,可加 TTL 机制 |
| 3 | 连接异常未处理 | MCP Server 不可用时整个服务崩溃 | app.py:108-111 中 get_mcp_tools() 用 try/except 优雅降级为空列表 |
🎯 STEP 5 ChromaDB 向量数据库
📌 操作: 使用 ChromaDB 作为向量存储后端,支持持久化、元数据关联和检索
① 🛠️ 所用技术
| 属性 | 内容 |
|---|---|
| 技术 | ChromaDB + DashScopeEmbeddings (text-embedding-v4) + HNSW 索引 |
| 分类 | 向量数据库 / 语义检索 / 近似最近邻搜索(ANN) |
| 角色 | 存储文档片段的向量表示,提供基于语义相似度的 Top-K 检索 |
② 💡 为什么选择 ChromaDB
项目约束:
- 需要在 Windows 环境无额外依赖运行
- 数据量小(< 10 万向量),不需要分布式
- 需要与 LangChain 无缝集成
- 需要持久化到本地磁盘,重启不丢失
匹配原因:
- ChromaDB 是纯 Python 实现(基于 SQLite3 + DuckDB),Windows 开箱即用,无需 Docker
- 内建 HNSW 索引,在 1 万级数据量上检索延迟 < 10ms
- 实现了 LangChain 的
VectorStore基类,as_retriever()一行代码转检索器 - 数据存储在
./rag/chroma_db/目录,重启自动加载,零运维
Trade-off:
✅ 选择了:嵌入式向量数据库(与应用同进程)
❌ 放弃了:独立的向量数据库服务(如 Milvus),无法横向扩展
⚖️ 值得是因为:项目数据量不会突破单机极限,嵌入式架构消除了网络延迟和运维负担
③ 📚 技术深度剖析
【核心原理】HNSW(Hierarchical Navigable Small World)索引
HNSW 是 ChromaDB 内部使用的近似最近邻搜索算法,核心思想来自现实世界的"六度分隔"理论:
- 构建一个多层图结构,上层节点少(高速公路),下层节点多(街道)
- 搜索时从最上层开始,快速定位大致区域,逐层向下细化
- 每一层只检查少数节点(候选集大小 =
ef_search参数)
时间复杂度:O(log N),远优于暴力搜索的 O(N)
【关键概念】
概念 1:向量距离度量
ChromaDB 默认使用余弦相似度衡量向量距离:
余弦相似度 = cos(θ) = (A · B) / (||A|| × ||B||)
值域: [-1, 1],1 表示完全相同方向(语义一致),0 表示正交(无关),-1 表示完全相反
对于 Embedding 模型,所有向量都位于单位超球面上,所以余弦相似度简化为向量的点积。
概念 2:Collection(集合)
ChromaDB 用 Collection 组织向量,类似关系数据库的"表"。本项目的 Collection 名为 agent_knowledge,所有文档片段的向量、文本、元数据都存储在这个 Collection 中。
⚠️ 常见陷阱
| # | 陷阱 | 后果 | 避免/解决方法 |
|---|---|---|---|
| 1 | ChromaDB 版本不兼容 | 升级后旧持久化数据无法读取 | 锁定版本号,本项目使用 langchain-chroma 集成包管理版本 |
| 2 | 集合名重复创建 | 报错或数据覆盖 | 使用固定的 collection_name,每次获取而非新建 |
🎯 STEP 6 Streamlit Web 界面
📌 操作: 构建双页面 Streamlit 应用 — 智能客服(用户端)+ 知识库管理(商家端)
① 🛠️ 所用技术
| 属性 | 内容 |
|---|---|
| 技术 | Streamlit + Custom CSS + Session State + Chat UI 组件 |
| 分类 | Python Web 框架 / 数据应用框架 / UI 框架 |
| 角色 | 提供零前端代码的 Web 界面,双页面架构 + 侧边栏导航 |
② 💡 为什么选择 Streamlit
项目约束:
- 团队没有前端开发者,只有 Python 能力
- 需要快速原型交付(2-3 天完成 UI)
- 需要 Chat UI 组件(聊天气泡、输入框、历史消息展示)
- 不需要 SEO 或高并发
匹配原因:
- Streamlit 的
st.chat_message()和st.chat_input()提供了开箱即用的聊天 UI - 纯 Python 编写,不需要 HTML/JS/CSS(虽然本项目也注入了少量自定义 CSS)
st.cache_resource装饰器提供了跨用户共享的重量级对象缓存(MCP 连接、配置实例)st.rerun()机制简化了状态更新后的 UI 刷新
Trade-off:
✅ 选择了:Python 原生的 Web 开发体验,极短的学习曲线
❌ 放弃了:React/Vue 的灵活性、SEO 能力、高并发性能
⚖️ 值得是因为:这是一个内部客服工具,并发量不超过 10 人,不需要 SEO,
Streamlit 将开发效率提升了 5-10 倍
3. 项目结构
langchain-rag-agent/
│
├── app.py # 🔴 Streamlit Web 入口
│ ├── page_customer_service() # 页面一:智能客服(用户端)
│ ├── page_knowledge_management() # 页面二:知识库管理(商家端)
│ └── main() # 主入口 + 导航 + 生命周期
│
├── config.py # 🟡 全局配置(模块级单例 CONFIG)
│ └── class Config # API Key、模型参数、MCP 连接、IP 检测
│
├── .env # 🟢 环境变量(API Key、高德 Key、IP)
│
├── rag/ # 🔵 RAG 核心模块
│ ├── __init__.py # 导出 RAGConfig, KnowledgeBaseBuilder, RagAgent
│ ├── config.py # RAG 专用配置(分割参数、检索参数、路径)
│ ├── agent.py # RagAgent:Agent 创建 + 异步问答 + 历史管理
│ ├── knowledge_base.py # KnowledgeBaseBuilder + DedupService + TextSplitter
│ ├── loaders.py # DocumentLoader:工厂模式,支持 TXT/PDF
│ └── history.py # FileChatHistory + HistoryManager:会话持久化
│
├── mcp_service/ # 🟣 MCP 客户端
│ ├── __init__.py
│ └── mcp_client.py # McpClientManager:单例模式 MCP 客户端
│
├── data/ # 📁 知识库源文件
│ ├── 维护保养.txt
│ ├── 扫地机器人100问.txt
│ ├── 扫地机器人100问2.txt
│ ├── 扫拖一体机器人100问.txt
│ ├── 选购指南.txt
│ ├── 故障排除.txt
│ └── 扫地机器人100问.pdf
│
├── rag/chroma_db/ # 💾 ChromaDB 向量数据(自动生成)
├── rag/chat_history/ # 💬 会话历史文件(自动生成)
├── rag/md5.text # 🔑 MD5 去重记录(自动生成)
│
└── .venv/ # 🐍 Python 虚拟环境
数据流图:
用户输入问题
│
▼
┌─────────────┐ ┌──────────────────┐ ┌────────────┐
│ Streamlit │────→│ RagAgent │────→│ LLM │
│ app.py │ │ agent.py │ │ Qwen3-Max │
└─────────────┘ └──┬───────┬───────┘ └────────────┘
│ │ │
Tool │ Tool │ ToolCall
① │ ② │ Decision
│ │ │
┌───────────┘ └──────────┐ │
▼ ▼ ▼
┌───────────────┐ ┌──────────────┐ ┌──────┐
│ ChromaDB │ │ MCP Server │ │ 直接 │
│ 向量检索 │ │ 高德地图 │ │ 回复 │
│ (语义搜索) │ │ (位置/天气) │ │ │
└───────────────┘ └──────────────┘ └──────┘
│ │ │
└────────────────────────┴──────────────┘
│
▼
┌────────────────┐
│ 合成最终回复 │
│ 返回给用户 │
└────────────────┘
4. 快速开始
环境要求
- Python 3.11+
- Windows / macOS / Linux
安装与运行
# 1. 克隆项目
cd langchain-rag-agent
# 2. 创建虚拟环境
python -m venv .venv
.venv\Scripts\activate # Windows
# source .venv/bin/activate # macOS/Linux
# 3. 安装依赖
pip install streamlit langchain langchain-community langchain-chroma \
langchain-mcp-adapters langchain-text-splitters python-dotenv \
dashscope httpx pypdf chromadb
# 4. 配置环境变量(.env 文件)
# DASHSCOPE_API_KEY=你的阿里百炼API密钥
# IP=你的公网IP(可选,留空自动检测)
# 5. 构建知识库(首次运行)
python -c "from rag import KnowledgeBaseBuilder; kb = KnowledgeBaseBuilder(); kb.build_from_directory()"
# 6. 启动应用
streamlit run app.py
使用指南
| 操作 | 说明 |
|---|---|
| 💬 智能客服 | 自然语言提问,支持保养/故障/选购/天气关联建议 |
| ⚡ 快捷提问 | 侧边栏预置 5 个常见问题,一键发送 |
| 📥 导出对话 | 支持导出单条问答或全部对话记录为 Markdown |
| 📚 知识库管理 | 上传 PDF/TXT 文档,自动分割去重入库 |
| 🔄 重建知识库 | 一键从 data 目录全量重建 |
5. 知识延伸与学习路径
🗺️ 完整学习路径
前置知识 本项目核心技术 进阶方向
─────────────────────────────────────────────────────────────────────
Python 基础 (asyncio, → RAG (检索增强生成) → Advanced RAG
dataclass, typing) (Self-RAG,
LangChain Agent CRAG, GraphRAG)
Prompt Engineering → (ReAct 循环, 工具调用)
基础 → LangGraph
MCP (模型上下文协议) (复杂多步工作流)
向量/Embedding 概念 → (标准化工具接入)
→ Multi-Agent
ChromaDB (向量数据库) (多智能体协作)
→ Agent 可观测性
Streamlit (Python Web) (LangSmith/LangFuse)
📖 推荐学习资源
| 类型 | 资源 | 说明 | 链接 |
|---|---|---|---|
| 📖 官方文档 | LangChain Agent 文档 | Agent 创建、工具定义、历史管理的权威参考 | https://python.langchain.com/docs/concepts/agents/ |
| 📖 官方文档 | MCP 协议规范 | 理解模型上下文协议的完整架构 | https://modelcontextprotocol.io/ |
| 📖 官方文档 | ChromaDB 使用指南 | 向量存储的增删改查和调优 | https://docs.trychroma.com/ |
| 📖 官方文档 | Streamlit Chat 组件 | 聊天 UI 的完整 API 参考 | https://docs.streamlit.io/develop/api-reference/chat |
| 📕 经典书籍 | 《Build a Large Language Model (From Scratch)》 | 理解 Embedding、Attention、Transformer 的底层原理 | Sebastian Raschka |
| 📝 论文 | Retrieval-Augmented Generation for Knowledge-Intensive NLP Tasks | RAG 原论文 (Lewis et al., 2020) | arXiv:2005.11401 |
| 💻 开源项目 | LangChain 官方示例库 | 数十个 Agent/RAG 的完整项目示例 | https://github.com/langchain-ai/langchain |
技术栈速查
| 层 | 技术 | 版本/型号 |
|---|---|---|
| 🖥️ Web 框架 | Streamlit | 1.x |
| 🧠 LLM | 阿里百炼 DashScope (Qwen3-Max) | qwen3-max |
| 📊 Embedding | 阿里百炼 DashScope | text-embedding-v4 |
| 🔍 向量数据库 | ChromaDB | (langchain-chroma) |
| 🔧 Agent 框架 | LangChain | create_agent() |
| 🔌 工具协议 | MCP (Model Context Protocol) | HTTP Transport |
| 🗺️ 外部服务 | 高德地图 MCP Server | 阿里百炼托管 |
| 💾 会话存储 | 本地 JSON 文件 | /rag/chat_history/ |
| 📄 文档处理 | PyPDF + RecursiveCharacterTextSplitter | Chunk 500 / Overlap 50 |
智净灵境 © 2026 · 基于 LangChain RAG + MCP 的智能客服系统
安装
This server does not publish a one-line install command.
Open the repository installation guide 新手?先看 mcpskill.org 教程 →