从零构建一个智能客服 AI Agent

6 阅读6分钟

从零构建一个智能客服 AI Agent:LangGraph + Chroma RAG + DeepSeek 实战

本文用一个完整项目,手把手拆解 AI Agent 的核心模式:ReAct 循环、Tool-Use、RAG 检索、多轮记忆,以及背后的设计取舍。


一、为什么做这个项目?

面试 AI Agent 开发岗时,有一道出现频率极高的问题:

"设计一个 AI 客服 Agent,说说你的架构。"

这道题考的不是你会调 API,而是你对 Agent 系统的整体设计能力

  • 怎么让 LLM 理解用户意图?
  • 怎么接入外部数据(FAQ、订单系统)?
  • 多轮对话的记忆怎么管理?
  • 出错了怎么办?

与其背八股,不如直接写一个能跑的项目。本文记录的就是这样一个实战——用 LangGraph 编排 Agent,用 Chroma 做 RAG,用 DeepSeek 做推理引擎。

最终代码在 GitHub 上:github.com/cuzz123/cus…


二、技术选型与为什么

选型原因
LangGraph(而非 LangChain Agent)LangChain Agent 是黑盒,LangGraph 的 StateGraph 让我能显式控制每一步——意图判断、工具调用、循环决策。面试时可以说 "我手写了 StateGraph 的 Node 和 Edge,而不是调了一个封装好的 AgentExecutor"。
Chroma(而非 FAISS / Pinecone)本地部署,零成本,和 LangChain 集成好。Chroma 内置 ONNX embedding 模型,不需要额外装 PyTorch 或 sentence-transformers,依赖极轻。
DeepSeek(而非 OpenAI)价格便宜(约 1/10),中文理解强,兼容 OpenAI 接口格式,切换成本低。

三、架构设计:ReAct 循环

整个 Agent 跑在一个 ReAct(Reasoning + Acting)循环 里:

用户输入
    │
    ▼
┌─────────────────────┐
│   Agent Node        │  ← LLM 判断:直接回复?还是调工具?
│   (DeepSeek)        │
└────────┬────────────┘
         │
    ┌────┴────┐
    ▼         ▼
 有工具调用   直接回复
    │           │
    ▼           ▼
┌────────┐   输出结果
│ Tools  │
│ Node   │  ← 执行 FAQ/订单/天气 查询
└───┬────┘
    │
    └──→ 回到 Agent Node,再次判断

关键设计决策

1. 为什么要循环?

一次工具调用的结果可能触发另一个工具。比如用户问 "帮我查一下我的订单物流"——先要查出订单号,再查物流,再回答。循环让 Agent 能连续调用多个工具。

2. 为什么用 StateGraph 而不是 Sequential Chain?

StateGraph 的节点之间可以有条件跳转(conditional edges),不是死板的线性流程。我的代码里:

def should_continue(state: MessagesState) -> Literal["tools", "__end__"]:
    """检查最后一条消息是否有工具调用"""
    last_msg = state["messages"][-1]
    if last_msg.tool_calls:
        return "tools"   # 去执行工具
    return END           # 直接回复用户

这 vs 传统的 if-else 路由:StateGraph 是图结构,天然支持复杂的分支和循环,而 if-else 只能处理简单的线性分支。


四、三个 Tool 的实现

1. FAQ 检索(RAG)

这是最核心的一个 Tool。用户问 "怎么退货?"、"保修期多久?" 这类问题时,需要从知识库里检索答案。

流程:

FAQ 文档 → 分块 → Embedding → Chroma 向量库 → 语义检索 → 返回 Top-K

代码实现(简化版):

def _build_faq_db():
    """构建 Chroma 向量库"""
    ef = embedding_functions.DefaultEmbeddingFunction()
    client = chromadb.PersistentClient(path="data/chroma_db")

    # 按问题-答案对分块
    collection.add(documents=chunks, ids=chunk_ids)
    return collection

def faq_search(query: str) -> str:
    collection = _build_faq_db()
    results = collection.query(query_texts=[query], n_results=3)
    return format_results(results)

设计取舍:

选项选了什么为什么
分块策略按 FAQ 的标题层级分块(200 chars)问题-答案对天然是一个完整语义单元
EmbeddingChroma 内置 ONNX(all-MiniLM-L6-v2)轻量,不依赖 PyTorch,~70MB
K 值Top-3太多会混杂不相关内容,太少可能漏掉
重排序V1 不做简单场景 Top-3 够用,V2 可加 Cohere Rerank

2. 订单查询

模拟调用后端订单系统的 API。实际生产环境就是调一个内部 HTTP 接口:

def query_order(order_id: str) -> str:
    """查询订单状态、物流、预计送达"""
    orders = _load_orders()  # 实际环境: requests.get("/api/orders/{id}")
    # ... 格式化返回

3. 天气查询

同样模拟第三方 API 调用。展示了 Agent 如何接入外部服务。


五、多轮对话记忆

LangGraph 内置了 MemorySaver(Checkpointer),它记录每次 invoke 的消息历史到内存中:

memory = MemorySaver()
app = workflow.compile(checkpointer=memory)

# 每次调用带上 thread_id
result = app.invoke(
    {"messages": [("human", user_input)]},
    config={"configurable": {"thread_id": "session_001"}},
)

为什么用 Checkpointer 而不是 ConversationBufferMemory?

  • Checkpointer 是 LangGraph 原生的,和 StateGraph 的图结构深度集成
  • 它是增量存储的,每次只保存状态变更,而不是整个消息列表
  • 恢复对话时只需要 thread_id,不需要手动维护 memory 对象

局限(V2 改进方向): 目前是进程内内存,重启就丢了。生产环境应换成 PostgresCheckpointerRedis 实现持久化。


六、兜底与容错

Agent 不能保证永远正确,但可以保证出错了也不崩。我做了三层兜底:

保护什么实现
LLM 调用失败API 超时、网络异常try/except → 返回降级回复
Tool 执行异常查询出错、数据格式异常每个 Tool 内部独立 try/except
无匹配结果FAQ 没找到答案返回友好提示,建议转人工

错误不是 bug,是功能。 面试官考你"Agent 出错怎么处理"时,能说出这三层兜底,比只说"try catch"高两个档次。


七、可以怎么给面试官讲这个项目?(面试话术)

如果面试官问:"说说你做过的这个项目"

30 秒 elevator pitch:

"我用 LangGraph 搭了一个智能客服 Agent,核心是一个 ReAct 循环:LLM 判断意图 → 调用 Tool → 收集结果 → 再判断。支持 RAG 搜索 FAQ、查询订单和天气,有多轮记忆和三层兜底机制。代码开源在 GitHub,500 多行 Python。"

如果追问:"为什么这么设计?"

  • "选 LangGraph 而不是 LangChain Agent,是因为我需要控制每一步的决策,而不是黑盒调用。StateGraph 让我能画出明确的 Node 和 Edge。"
  • "RAG 部分用 Chroma 做向量检索,而不是简单的关键词匹配,因为用户问法多样——'退货流程' 和 '怎么把东西退回去' 是同一个意思,语义检索才能覆盖。"
  • "三层容错是为了应对生产环境的不可靠——LLM 可能超时、数据库可能连不上、搜索可能没结果,每一层都有预案。"

如果被问"哪里可以改进?"

  • "当前记忆是内存级别的,重启丢失。V2 可以接 PostgresCheckpointer 做持久化。"
  • "没有做 Agent 的 Observability。可以集成 LangSmith 做 Trace,追踪每次 LLM 调用的耗时和 Token 消耗。"
  • "没有流式响应。可以用 .stream() 让用户看到逐字输出,体验更好。"
  • "没有加 Rate Limit 和 Cost 控制。DeepSeek 虽然便宜,但死循环会浪费钱,可以用最大迭代次数兜底。"

八、项目链接

GitHub:github.com/cuzz123/cus…

有什么问题或建议,欢迎在评论区讨论!


转行小贴士: 这个项目是为转行做AI Agent 开发的伙伴准备的第一个实战项目。按计划这套做完还会有 MCP Server 和多 Agent 协作系统,保持关注。