用 Python + Ollama + Chroma + LangChain 攒一个能记住上下文的客服机器人,其实没有想象中那么难
先说个场景:我在本地跑过不少开源大模型,也试过直接调用各种在线 API 来做问答。但真到了要做一个“能记住用户上一句说了什么”的客服系统时,才发现事情没有想象中那么简单。纯粹调用模型接口,只能做到一问一答,用户说“刚才那个问题再解释一下”,模型根本不知道“刚才”指的是什么。后来我把 Python、Ollama、Chroma、LangChain 这四个东西组合在一起,才真正把“多轮对话”这件事跑通。
这篇博文就是记录我从零开始搭建这套客服系统的全过程,重点放在整体的技术选型思路、环境准备、第一个能跑起来的多轮对话版本,以及我在实际开发中踩过的坑。如果你是刚接触 LangChain 或者本地部署大模型的新手,这篇文章能帮你少走不少弯路。如果你已经有一定基础,也可以重点看看第四部分的故障排查,很多问题是我翻了不少 issue 才搞明白的。
先说清楚这套组合是干什么的:Ollama 负责在本地把大模型跑起来,不依赖云端 API,数据不用出内网;LangChain 负责把“调用模型”这件事包装成标准的接口,同时提供了处理对话历史、管理 Prompt 的工具;Chroma 是一个轻量级的向量数据库,用来存放客服知识库的向量索引,让机器人能从文档里检索答案,而不是每次都瞎编。Python 则是把这三者粘合在一起的胶水语言。整体可以理解成:Python 是控制台,Ollama 是大脑,Chroma 是记忆仓库,LangChain 是神经系统。
1. 方案选型:为什么偏偏是这四个组件
1.1 为什么用 Ollama 而不是直接调云端 API
我最早做客服机器人原型的时候,用的是在线大模型 API。效果虽然不错,但有几个问题始终绕不开:第一是数据隐私,客服对话里经常包含用户手机号、订单号这类敏感信息,发送到云端总归不踏实;第二是成本,客服场景的请求量是不可控的,一旦并发上来,账单数字看得人心慌;第三是网络依赖,内网环境或者网络不稳定的情况下,API 调用延迟会直接影响用户体验。
Ollama 解决的就是这三个问题。它是一个本地模型运行时,安装之后会自动拉起一个本地的 HTTP 服务,默认监听 11434 端口。你只需要把模型下载到本地,后续所有推理都在本机完成。我用一台 32GB 内存、无独立显卡的机器实测过,跑 7B 级别的量化模型,生成速度大概在每秒 10 到 15 个 token 左右,做客服问答是够用的。如果有 NVIDIA 显卡,速度会快很多,但 CPU 也并不是完全不能跑。
Ollama 还有一个很方便的点:它对模型格式做了统一封装。你不需要自己处理 PyTorch 权重、分词器、推理脚本这些东西,一条命令就能把模型拉下来跑起来。这对快速验证想法特别有利,我可以先花十分钟把整个链路跑通,再回头优化细节。
1.2 为什么用 Chroma 而不是传统的数据库
客服系统要回答的很多问题,答案都藏在产品文档、帮助手册、FAQ 里。我的做法是把这些文档切分成小段,然后转成向量存起来,用户提问时先在向量库里做相似度检索,找到最相关的几个片段,再把这些片段和用户问题一起交给大模型生成答案。这种方案就是现在很流行的 RAG(检索增强生成)。
向量数据库的选择上,市面上有 Chroma、Milvus、Qdrant、Weaviate 等好几个。我选 Chroma 的理由非常简单:对于单机版客服系统这个场景,Chroma 是配置成本最低的。它是一个嵌入式数据库,不需要单独部署服务端,直接在 Python 进程内运行,数据落到本地文件。而 Milvus 和 Qdrant 虽然功能更强大、支持分布式部署,但需要额外维护一套服务,对一个小型客服系统来说有点杀鸡用牛刀了。
Chroma 的 API 设计也很直观,核心操作就三个:创建集合、往集合里添加文档、查询相似内容。我用一个不恰当的比喻来解释它和传统数据库的区别:传统数据库像是一个 Excel 表格,你往里存数据时要知道有哪些列;而 Chroma 像是一个图书馆的索引卡,你给它一本书,它自动帮你把书里的章节、段落编号、关键词排序整理好,你只需要说“我想找和这个主题相关的内容”,它就能把最相关的书页递给你。
1.3 LangChain 在这套系统里的真实价值
说实话,LangChain 刚出来的时候,我也觉得它的抽象太多,学起来费劲。但用多了之后,我承认它在几个关键环节上确实帮我省了不少事。
最实用的功能是多轮对话的消息历史管理。LangChain 提供了RunnableWithMessageHistory和ChatMessageHistory这样的组件,能够把每次对话的输入输出追加到一个存储里,在调用模型时自动拼接到 Prompt 中。如果没有这个能力,我自己得维护一个会话列表,每次请求时把所有历史消息找出来,手动格式化放到 Prompt 里,还要处理超过上下文长度时到底丢弃哪些历史的问题。LangChain 把这些基础逻辑封装好了,我只需要配置一个 session ID,就能实现多轮会话的隔离。
LangChain 还有一套统一的模型接口。今天用 Ollama 跑本地模型,明天想换成 OpenAI 的 GPT,只需要改一行代码切换ChatOllama和ChatOpenAI,下游的检索和 Prompt 逻辑完全不用动。这对以后系统迁移或者做双路备份非常利好。
当然,LangChain 也不是没有槽点。它的 API 在 0.1 到 0.2 版本之间有比较大的变化,网上搜到的很多旧教程在新版本上直接跑不通。我看到网上很多人都在讨论 LangChain 和 LangGraph 的区别,也有人在问 LangChain Agent 怎么用。以我目前做客服机器人这个项目来看,LangChain 自带的能力已经足够,LangGraph 那种复杂的状态机和多智能体编排,等以后需要构建更复杂的对话流程时再引入不迟。新手入门还是建议先把核心概念吃透。
2. 环境准备:从零开始搭起开发环境
2.1 Python 环境安装与配置
这套系统的所有代码都是用 Python 写的,所以第一步是把 Python 环境配置好。我推荐使用 Python 3.10 或 3.11 版本,太老的版本对 LangChain 新版本支持不好,太新的版本(比如 3.13)有些依赖库的预编译包可能还没跟上,装起来容易出问题。
Windows 用户直接去 Python 官网下载安装包,安装时务必勾选“Add Python to PATH”选项,不然命令行里敲python会提示找不到命令。macOS 用户建议用 Homebrew 安装:brew install python@3.11。Linux 用户用系统自带包管理器即可,比如 Ubuntu 上是sudo apt install python3.11。
装完之后打开终端(Windows 上是 CMD 或 PowerShell),输入python --version,看到熟悉的版本号输出,说明安装成功了。我习惯再确认一下 pip 的版本,pip --version,后续安装 Python 库就靠它了。
这里我有一个小建议:不要直接往系统 Python 里装依赖,否则项目多了以后各种包版本冲突真的很烦人。我用的是 Conda 来管理不同的项目环境,比如这个客服系统就单独建了一个环境:
conda create -n chat_cs python=3.11 conda activate chat_cs提示:如果公司网络限制比较多,pip 安装第三方库超时的话,可以临时切换为国内镜像源,比如清华源:
pip install -i https://pypi.tuna.tsinghua.edu.cn/simple 包名。这只是把 Python 软件包的下载服务器改成访问更快的节点,属于常规操作。
2.2 Ollama 安装与模型下载
Ollama 的安装分两步:先装运行时,再拉模型。运行时安装很简单,去 Ollama 官网下载对应操作系统的安装包,或者用官方脚本安装。装完以后在终端输入ollama --version确认安装成功。
然后是模型下载。Ollama 支持很多开源模型,我用的主力是qwen2.5:7b或者llama3.1:8b。对于客服场景,这两个模型的指令跟随能力和中文理解能力都算不错,而且参数量适中,普通机器跑得动。下载命令非常直观:
ollama pull qwen2.5:7b如果你经常碰到模型下载中断、速度很慢的情况,这里分享两个合规、靠谱的解决思路。
第一个思路是换源。Ollama 的模型下载地址默认指向海外仓库,国内访问确实不稳定。Ollama 支持通过环境变量OLLAMA_MODELS修改模型存储位置,也有社区维护的镜像站点可以配置。也可以检查一下自己所在网络环境是否本身就存在限制,考虑使用更稳定的网络方式来下载。
第二个思路是离线导入。如果在线下载实在搞不定,可以去一些国内公开的大模型社区(比如某些提供模型文件直传的站点)把模型文件下载下来,然后用ollama create命令从本地配置文件导入到 Ollama 中。这个方法虽然稍微麻烦一点,但胜在稳定、可控,而且不受网络波动影响。我离线导入过一个 4.7GB 的量化模型文件,按官方文档写一个Modelfile,一句一行的配置,然后执行导入,实际上手并不复杂。
模型下载完成后,用ollama list可以看到已安装的模型列表和占用空间。我建议只保留日常要用的 1 到 2 个模型,因为 7B 级别的模型动辄几个 GB,硬盘空间消耗不小。
2.3 Chroma 与 LangChain 的安装
Chroma 和 LangChain 都通过 pip 安装。但这里有个版本兼容的问题,我在第一次安装时就吃过亏。
如果按照网上的老教程直接pip install chromadb langchain,大概率会装到最新版本,而最新版本的 LangChain(0.3.x 之后)把很多底层依赖拆分到了不同的子包中,比如langchain-community、langchain-chroma,导致代码里from langchain.vectorstores import Chroma直接报错。
我的建议是装指定版本,并且按照依赖关系分开装:
pip install langchain==0.2.14 pip install langchain-community==0.2.12 pip install langchain-chroma==0.1.3 pip install chromadb==0.5.5 pip install ollama这里简单解释一下为什么装langchain-chroma这个子包。0.2 版本之后的 LangChain 把向量数据库的集成从主包里拆出来了,单独作为一个依赖包维护。如果你直接装最新版的chromadb配合最新版的langchain,在导入时经常会碰到 API 签名对不上的问题。固定版本虽然看起来不潮流,但对于一个要稳定跑起来的项目来说,锁定版本永远是第一位的。等模型和代码都跑通了,再去升版本也不迟。
到这里,开发环境就已经准备好了。我习惯先做一次冒烟测试,写几行简单脚本确认每个组件都能正常调用:
import ollama response = ollama.chat( model="qwen2.5:7b", messages=[{"role": "user", "content": "你好,做一个自我介绍"}] ) print(response["message"]["content"])如果终端能正常输出模型的回复,说明 Python、Ollama、模型这三层链路已经打通了。
3. 第一个多轮对话版本:从无状态到有记忆
3.1 无状态对话的局限
客服系统的核心交互形式是多轮对话。但很多人起步时写的第一版 QA 代码是无状态的,也就是每次请求都是独立的,不带任何历史信息:
from langchain_community.chat_models import ChatOllama llm = ChatOllama(model="qwen2.5:7b") response = llm.invoke("我今天买了一个电子产品出了问题,需要退货") print(response.content)这种实现跑起来很简单,但用户体验很差。用户接着问“怎么申请退货流程?”时,模型没有前文信息,只能靠自己的常识回答,可能完全不记得用户前面说了“电子产品出了问题”。要让机器人表现得像个真人客服,必须把对话历史传给模型。
3.2 用 LangChain 管理对话历史
LangChain 提供的做法是把“对话历史存储”和“模型调用”组合在一起。核心思路是:
- 用
ChatMessageHistory对象保存某个用户会话的消息列表; - 每次调用模型之前,把历史消息和当前问题拼成消息组;
- 调用模型返回答案后,把新的一问一答追加到历史列表里。
这里的会话隔离很重要。多个用户同时在线聊天时,不能所有用户共享同一份历史,那就串戏了。所以还需要一个 HashMap(或者数据库表)来按 session ID 存储不同的ChatMessageHistory实例。
我画一张简化的流程图帮大家理解这个流程——用户带着 session_id 发来问题,系统拿出这个 session 对应的聊天记录,连同本次问题一起打包发给大模型,模型返回答案后再更新聊天记录。注意,这里所有收发都通过 LangChain 的接口完成,所以代码本身不依赖具体某个模型。
3.3 最小可行性代码实现
下面是一份用 LangChain + Ollama 实现的最简多轮对话代码,你可以直接复制运行看看效果。
from langchain_community.chat_models import ChatOllama from langchain.memory import ChatMessageHistory from langchain.schema import HumanMessage, AIMessage # 1. 初始化本地大模型 llm = ChatOllama( model="qwen2.5:7b", temperature=0.7, num_predict=2048, ) # 2. 用字典保存不同 session 的聊天记录 sessions = {} def get_session_history(session_id: str) -> ChatMessageHistory: if session_id not in sessions: sessions[session_id] = ChatMessageHistory() return sessions[session_id] def chat(session_id: str, user_input: str) -> str: history = get_session_history(session_id) # 3. 构造完整消息列表:先历史,后当前 messages = history.messages + [HumanMessage(content=user_input)] # 4. 调用模型 ai_response = llm.invoke(messages) # 5. 更新历史 history.add_user_message(user_input) history.add_ai_message(ai_response.content) return ai_response.content # 测试:模拟同一个用户连续提问 print(chat("user_001", "我想退货,怎么操作?")) print(chat("user_001", "需要什么条件吗?")) print(chat("user_002", "我要开发票"))注意第二步和第五步的设计:history.add_user_message和add_ai_message是在模型调用结束后才执行的。如果在调用前就把当前问题加进 history,再结合第三步的history.messages + [HumanMessage(content=user_input)],就会造成当前问题出现两次,模型就会懵。这个顺序问题是我最早踩过的坑之一,建议大家写的时候特别注意。
另外需要注意,这段代码把所有聊天记录放在内存字典里,服务重启之后历史就丢了。对于生产环境,可以改成把聊天记录存到 Redis 或者数据库,但核心逻辑完全一样。我做客服系统的第一版就是这么跑的,先验证业务逻辑,再考虑持久化。
3.4 关于上下文长度的取舍
把整段历史一股脑全传给模型,短暂使用没有问题,但聊得久了,历史消息数量会越来越大。大模型的上下文窗口(Context Window)是有限的,而且输入越长的 Token 数量,推理耗时和费用也越高(本地部署主要是耗时)。所以必须对历史做截断或摘要。
我采用的策略是只保留最近 N 轮对话。比如只取最近的 6 轮(12 条消息),超过的部分全部丢弃。实现也很简单:
MAX_TURNS = 6 def trim_history(history: ChatMessageHistory): messages = history.messages if len(messages) > MAX_TURNS * 2: history.messages = messages[-MAX_TURNS * 2:]在chat函数最开始调用一下trim_history(history)就可以了。
如果要更精细地控制,还可以按 Token 数而不是消息条数来裁剪,这个后面聊到 Chroma 的时候再展开。对于最简单的客服场景,按轮数截断已经足够用了。
4. 接入 Chroma:让机器人学会查资料
4.1 什么是 Embedding,为什么要向量化
到目前为止,机器人已经能记住上下文了,但它回答问题时依然只能靠“脑袋里的知识”。如果用户问的是你们公司的退换货政策、产品使用说明,模型肯定不知道,因为这些知识不在它的训练数据里。这时候就需要把公司的知识库喂给模型。
直接喂全文是行不通的,模型上下文放不下。所以通常的做法是:把知识库文档切成很多小段(几百字一段),然后用 Embedding 模型把每一段变成一个向量。通俗来说,Embedding 就是“用一串数字表示一段文字的意思”。意思相近的两段话,向量之间的距离(比如余弦相似度)也会比较近。
用户来提问的时候,系统先把用户的问题转成向量,再到 Chroma 里查找和这个向量最接近的前 K 个文档片段。找到之后,把这些片段作为“参考资料”和用户问题一起交给大模型。模型可以根据参考资料给出贴近事实的回答,而不是漫无边际地编造。这就是 RAG 的基本思路。
4.2 用 Chroma 构建知识库索引
Chroma 在 LangChain 里的使用方式非常友好。下面演示一个完整流程:先创建集合,再把一批文档添加进去。我假设你的知识库在docs/目录下,每个文件是一个 Markdown 或 TXT 文档。
from langchain_community.embeddings import OllamaEmbeddings from langchain_community.vectorstores import Chroma from langchain.text_splitter import RecursiveCharacterTextSplitter from langchain_community.document_loaders import DirectoryLoader # 1. 加载文档 loader = DirectoryLoader("docs/", glob="**/*.md") documents = loader.load() # 2. 切分文档 text_splitter = RecursiveCharacterTextSplitter( chunk_size=500, chunk_overlap=50, ) splits = text_splitter.split_documents(documents) # 3. 初始化 Embedding 模型 embeddings = OllamaEmbeddings(model="qwen2.5:7b") # 4. 构建向量库并持久化 vectorstore = Chroma.from_documents( documents=splits, embedding=embeddings, persist_directory="./chroma_db", )这段代码里有三个关键参数值得展开讲一下。
第一个是chunk_size=500,也就是每段文本控制在 500 个字符左右。这个数值不是随便定的,太小了会导致语义不完整,太大了又会超出模型上下文窗口。我实测过在客服文档场景下,500 到 800 字是一个比较合适的区间,既能保持语义完整,也能保证单次拼接进 Prompt 的 Token 数不会太多。
第二个是chunk_overlap=50,表示相邻两个片段之间重叠 50 个字符。这是为了避免一个完整句子恰好被拦腰切断。比如“产品保修期为一年,一年内出现非人为损坏可免费维修”,如果被切成了“产品保修期为一年,一年内出现”和“非人为损坏可免费维修”,后一段单独拿出来检索时含义就不完整了。加一点重叠能有效降低这种问题。
第三个是persist_directory="./chroma_db",指定数据库落盘目录。首次执行这段代码后,向量库会持久化到磁盘,下次启动不需要重新加载和 Embedding 一遍,直接Chroma(persist_directory="./chroma_db", embedding_function=embeddings)就能加载。
注意:LangChain 0.2.x 版本中,
Chroma.from_documents默认会持久化,但建议显式传入persist_directory参数,避免不同版本行为不一致导致数据丢失。另外,Embedding 模型与提问时使用的模型推荐保持一致,否则向量空间不一致,检索效果会大打折扣。
4.3 把检索结果拼接到对话流程里
有了向量库之后,多轮对话的核心逻辑就要升级了。用户发来消息时,系统先做以下几步:
- 在 Chroma 里搜索与用户问题最相关的 3~5 个文档片段;
- 把这些片段拼接成一个“参考资料”字符串;
- 拼装 Prompt,告诉模型“以下是从知识库中检索到的内容,请基于这些信息回答用户问题”;
- 携带历史消息一起调用模型。
这里有一个很关键的工程点:检索的时候应该用当前这一轮的用户问题去检索,而不是把历史消息也拼进去。原因是用户当前的问题往往是检索意图最明确的表述,历史消息里的信息可能很零散,混在一起反而降低检索精度。比如用户之前抱怨了半天物流慢,当前这句“怎么退货”才是核心诉求。
拼装 Prompt 的代码示例如下:
def build_prompt(question: str, retrieved_docs): context = "\n\n".join([doc.page_content for doc in retrieved_docs]) prompt = f"""你是一名专业的客服助理。请根据以下知识库内容回答用户问题。 【知识库内容】 {context} 【用户的当前问题】 {question} 请用友好、简洁的语言回答。如果知识库中没有相关信息,请如实说明,不要编造。""" return prompt至于如何做相似度检索,LangChain 封装得比较好了:
retriever = vectorstore.as_retriever(search_kwargs={"k": 3}) docs = retriever.invoke(user_input) prompt = build_prompt(user_input, docs) full_messages = history.messages + [HumanMessage(content=prompt)]这样,整个客服系统的信息链路就完整了:知识库 → Chroma 检索 → 历史对话 → Prompt 组装 → Ollama 模型推理 → 返回答案 → 更新历史。用户问的问题,系统能结合自己知识库里“私有”的资料来回答,而且能记住多轮上下文。
4.4 Chroma 以外的向量数据库什么时候考虑
不少读者可能会问:网上关于 Milvus、Qdrant 的讨论也很多,我用 Chroma 是不是不够“先进”?
我理解这种疑虑,但实际选型要看场景。如果只是做一个中小型客服系统,知识库文档量在几万段以内,并发查询量不高,Chroma 完全够用,而且部署简单到几乎没有成本。但如果你要做的是大规模知识库搜索,比如千万级文档、高并发查询,或者需要水平扩展、分布式部署,那确实应该考虑 Qdrant 或 Milvus。Qdrant 的 Rust 底层性能非常强,Milvus 在 GPU 加速、多副本方面做得比较深入。
我的建议是:先把 Chroma 和整套系统跑通,把业务逻辑验证好。如果未来遇到性能瓶颈,再把存储层从 Chroma 替换为 Qdrant,因为上层 LangChain 的接口是统一的,替换成本主要在数据迁移上,代码逻辑几乎不用改。
5. 常见问题与排查技巧实录
5.1 Ollama 模型下载失败或速度慢
这个问题太常见了。如果你执行ollama pull时一直卡住或者报连接超时,首先看看模型存储目录是否空间不足,用ollama list查看当前占用。其次检查本地网络到默认仓库的连接状况,确实不稳定的情况下,可以考虑使用国内社区提供的镜像源(配置方式在ollama serve的文档里有),或者从第三方下载模型文件后离线导入。
这里补充一个离线导入的具体流程,因为我认为这条路径最稳定。
- 在公开模型社区找到对应模型的 GGUF 格式文件,下载到本地;
- 写一个
Modelfile,内容大致为:FROM ./qwen2.5-7b-instruct-q4_k_m.gguf - 执行
ollama create qwen2.5 -f Modelfile,等待导入完成; - 执行
ollama list确认模型已就绪。
这个方式避开了在线下载的不稳定因素,而且只要文件完整,导入基本不会失败。
提示:模型文件动辄几个 GB,下载时要留意磁盘剩余空间。另外,量化等级 Q4_K_M 是性价比比较高的选择,模型体积小、损失少。
5.2 LangChain 导入报错“Cannot import name Chroma”
这个报错在 0.2.x 版本比较常见,原因是向量存储的包被拆分到了langchain-chroma。解决方法是先确认安装的是哪个版本:
pip show langchain-chroma如果没装,执行pip install langchain-chroma==0.1.3。导入语句也改成:
from langchain_chroma import Chroma而不是from langchain.vectorstores import Chroma。这是 LangChain 逐步模块化的必然结果,以后可能有更多组件会被拆出来,记住一个原则:遇到导入报错时,先查对应子包是否已安装,再看版本兼容性。
5.3 向量库检索结果不相关
这个问题通常出在 Embedding 模型和数据切分方式上。
- 如果你用的 Embedding 模型和问答模型是同一个 Ollama 上的模型,但该模型不支持优质的中文语义表示,检索效果会打折。建议换用专门的 Embedding 模型,比如
nomic-embed-text或bge-m3,在 Ollama 上都能直接拉取。 chunk_size设置不合理会导致检索片段太碎或太笼统。可以尝试把 500 改成 300 和 800 各测一轮,用几组典型问题做对比,看返回的文档片段是否包含真正的答案。- 如果所有检索结果都差不多,可能是知识库本身的内容质量不高,或者每段文本之间区分度低。客服场景的文档往往口语化和重复内容较多,建议清洗一遍数据再建库。
5.4 多轮对话中上下文混淆
多轮对话最容易出现的问题是模型搞混了“历史里的用户”和“当前提问的用户”。检查点有四个:
- 是否每次会话传入的
session_id稳定不变; - 消息历史是否使用了
ChatMessageHistory而不是普通 list; - 模型调用时,消息列表的顺序是否为“先历史后当前”;
- 是否出现了历史消息里用户曾经问过的问题被当成当前问题重复处理。
我在一次调试中还发现,如果服务端开了多线程并发处理同一个 session 的请求,会存在轻微的竞态问题,导致消息顺序错乱。解决办法是在内存字典的读写操作上加锁,或者把 session 状态放到具备原子操作的 Redis 中。客服系统并发量通常不会特别大,但提前考虑到这一点能省去后续很多麻烦。
6. 系统优化的几个方向
第一版跑通之后,我强烈建议按下面的顺序逐步优化。
第一,接入更专业的 Embedding 模型。客服场景下,语义相似度的准确性直接影响检索质量。我在实测中把qwen2.5:7b作为 Embedding 模型切换到bge-m3后,检索相关性有明显提升,而且bge-m3的推理速度更快,占用更小。
第二,增加知识库的热更新能力。现在代码是启动时一次性加载所有文档。真实场景中,产品政策经常变动,总不能每次更新文档都重启服务。可以考虑把“新增文档 → 切分 → Embedding → 写入 Chroma”的流程做成一个独立的接口,运营同学更新完文档后调用一下就能完成增量更新。Chroma 的add_documents方法天然支持增量写入,核心代码并不复杂。
第三,对 Prompt 做更精细的设计。客服回答的语气、长度、是否允许转人工,这些业务约束都应该体现在 Prompt 里。更高级的做法是用LangChain的PromptTemplate把系统人设、知识库上下文、历史对话、用户问题四个部分明确区分开,并给每个部分不同的 System 和 User 角色标识。模型对角色区分越清楚,越不容易“跑偏”。
第四,评估并决定是否引入 LangGraph。LangChain 本身已经支持基本的多轮对话和历史管理,但如果你需要构建更复杂的客服流程,比如多步骤意图识别、工具调用(查订单状态、转接人工)、条件分支等,LangGraph 的状态图机制会更合适。它把对话流程建模为一个有向图,每个节点是一个处理逻辑,边是状态转移条件,非常贴合“可配置、可观测”这个需求。我在我的项目里暂时没上 LangGraph,因为当前场景用简单的链条就能解决。但如果你预见到流程复杂度会快速上升,尽早了解 LangGraph 不是坏事。
在我自己把这条链路完整跑通之前,说实话,我对“本地大模型能不能做客服机器人”是持怀疑态度的。但实际用下来,Python + Ollama + Chroma + LangChain 这套组合的配合比我想象中更顺畅。尤其是 Ollama 把本地模型调用简化成了几行代码,Chroma 把知识库存储和检索封装成了现成接口,LangChain 把模型切换和使用体验统一之后,整个开发过程的核心难点就从“不会调 API”转移到了“如何设计一个好的 Prompt”和“如何整理知识库内容”上。
最后再分享一个小技巧:做知识库相关开发时,准备一组覆盖核心业务场景的测试问题清单,每次调整切分参数、更换模型或者修改 Prompt 之后,都用同一组问题跑一遍回归测试,人工判断回答质量。这样看起来传统,但比什么自动化评估都好使,能帮你快速感知到某个改动到底是变好了还是变差了。我在开发这套客服系统时,这个习惯帮我少走了很多弯路。