Appearance
前面十篇的零件到这里全部拼起来。我们来做一个「智扫通机器人智能客服」——一个扫地机器人品牌的客服 Agent:能查产品知识库(RAG)、查天气/用户信息(工具)、记住多轮对话(记忆),还带日志监控(中间件)。这就是一个麻雀虽小五脏俱全的真实 Agent。顶部可切换 Python / Node.js。
整体设计
一句话概括架构:一个 Agent + 三样能力 + 记忆。
Agent 的大脑是大模型,它手里握着三类工具:search_kb(把第六、七篇的 RAG 封装成一个工具,用来查产品资料)、get_weather(外部接口示例)、get_user_info(查用户信息示例)。中间件负责日志监控,checkpointer 负责按用户记住对话。用户问什么,Agent 自己决定调哪个工具。
关键思路:把 RAG 变成 Agent 的一个工具。 之前 RAG 是主流程,现在它降格成 Agent 手里的一件「武器」——遇到产品问题才掏出来用。这是 RAG 和 Agent 结合的标准姿势。
第一步:把 RAG 封装成工具
复用前几篇的检索链,包成一个函数工具。工具的说明要写清楚「什么时候用它」,模型才会在该用的时候调它。
python
import os
from langchain_openai import ChatOpenAI, OpenAIEmbeddings
from langchain_community.document_loaders import TextLoader
from langchain_text_splitters import RecursiveCharacterTextSplitter
from langchain_core.vectorstores import InMemoryVectorStore
from langchain_core.tools import tool
DASHSCOPE = "https://dashscope.aliyuncs.com/compatible-mode/v1"
KEY = os.getenv("DASHSCOPE_API_KEY")
# 启动时把产品文档建成检索器(真实项目会用持久化的 Chroma)
embeddings = OpenAIEmbeddings(model="text-embedding-v3", api_key=KEY, base_url=DASHSCOPE)
docs = TextLoader("扫地机器人100问.txt", encoding="utf-8").load()
chunks = RecursiveCharacterTextSplitter(chunk_size=300, chunk_overlap=50).split_documents(docs)
_retriever = InMemoryVectorStore.from_documents(chunks, embeddings).as_retriever(search_kwargs={"k": 3})
@tool
def search_kb(query: str) -> str:
"""当用户询问扫地机器人的功能、使用、故障、保养等产品问题时,用它检索知识库。"""
results = _retriever.invoke(query)
return "\n\n".join(d.page_content for d in results) or "知识库中没有相关资料。"javascript
import { ChatOpenAI, OpenAIEmbeddings } from "@langchain/openai";
import { TextLoader } from "@langchain/classic/document_loaders/fs/text";
import { RecursiveCharacterTextSplitter } from "@langchain/textsplitters";
import { MemoryVectorStore } from "@langchain/classic/vectorstores/memory";
import { tool } from "@langchain/core/tools";
import { z } from "zod";
const DASHSCOPE = "https://dashscope.aliyuncs.com/compatible-mode/v1";
const KEY = process.env.DASHSCOPE_API_KEY;
// 启动时把产品文档建成检索器
const embeddings = new OpenAIEmbeddings({ model: "text-embedding-v3", apiKey: KEY, configuration: { baseURL: DASHSCOPE } });
const docs = await new TextLoader("扫地机器人100问.txt").load();
const chunks = await new RecursiveCharacterTextSplitter({ chunkSize: 300, chunkOverlap: 50 }).splitDocuments(docs);
const retriever = (await MemoryVectorStore.fromDocuments(chunks, embeddings)).asRetriever(3);
const searchKb = tool(
async ({ query }) => {
const results = await retriever.invoke(query);
return results.map((d) => d.pageContent).join("\n\n") || "知识库中没有相关资料。";
},
{
name: "search_kb",
description: "当用户询问扫地机器人的功能、使用、故障、保养等产品问题时,用它检索知识库。",
schema: z.object({ query: z.string() }),
}
);第二步:其它工具
再加两个示例工具,代表「调外部接口」和「查内部数据」。真实项目里换成你的天气 API、用户系统即可。
python
@tool
def get_weather(city: str) -> str:
"""查询某城市天气,用于判断是否适合扫地/拖地。"""
return f"{city}今天晴,湿度低,适合拖地。"
@tool
def get_user_info(user_id: str) -> str:
"""根据用户 ID 查询其购买的机型和保修状态。"""
return f"用户 {user_id}:机型 X20 Pro,在保修期内。"javascript
const getWeather = tool(async ({ city }) => `${city}今天晴,湿度低,适合拖地。`, {
name: "get_weather",
description: "查询某城市天气,用于判断是否适合扫地/拖地。",
schema: z.object({ city: z.string() }),
});
const getUserInfo = tool(async ({ userId }) => `用户 ${userId}:机型 X20 Pro,在保修期内。`, {
name: "get_user_info",
description: "根据用户 ID 查询其购买的机型和保修状态。",
schema: z.object({ userId: z.string() }),
});第三步:中间件(日志监控)
沿用上一篇的做法,加一个记录模型调用和工具调用的中间件。
python
from langchain.agents.middleware import before_model, wrap_tool_call
@before_model
def log_model(state, runtime):
print(f"[LOG] 调用模型,消息数={len(state['messages'])}")
return None
@wrap_tool_call
def monitor_tool(request, handler):
print(f"[LOG] 调用工具:{request.tool_call['name']} 参数={request.tool_call['args']}")
return handler(request)javascript
import { createMiddleware } from "langchain";
const logging = createMiddleware({
name: "Logging",
beforeModel: (state) => {
console.log(`[LOG] 调用模型,消息数=${state.messages.length}`);
return undefined;
},
wrapToolCall: async (request, handler) => {
console.log(`[LOG] 调用工具:${request.toolCall.name} 参数=${JSON.stringify(request.toolCall.args)}`);
return handler(request);
},
});第四步:组装 Agent 并加记忆
把模型、三个工具、中间件、系统提示词、checkpointer 全部交给 create_agent。checkpointer 让它按 thread_id 记住每个用户的对话。
python
from langchain.agents import create_agent
from langgraph.checkpoint.memory import InMemorySaver
model = ChatOpenAI(model="qwen-plus", api_key=KEY, base_url=DASHSCOPE, temperature=0)
SYSTEM = (
"你是「智扫通」扫地机器人的智能客服。"
"产品相关问题必须调用 search_kb 查证后再回答,不要凭空作答;"
"涉及用户机型/保修用 get_user_info;涉及清洁建议可用 get_weather。"
"回答简洁、亲切。"
)
agent = create_agent(
model=model,
tools=[search_kb, get_weather, get_user_info],
system_prompt=SYSTEM,
middleware=[log_model, monitor_tool],
checkpointer=InMemorySaver(),
)
def chat(text: str, user: str):
cfg = {"configurable": {"thread_id": user}}
result = agent.invoke({"messages": [{"role": "user", "content": text}]}, cfg)
return result["messages"][-1].content
if __name__ == "__main__":
print(chat("我的滤网多久洗一次?", user="u1"))
print(chat("我是用户 U8899,我的机器还在保修吗?", user="u1"))
print(chat("那我刚才问的滤网,你还记得吗?", user="u1")) # 记忆生效javascript
import { createAgent } from "langchain";
import { MemorySaver } from "@langchain/langgraph";
const model = new ChatOpenAI({ model: "qwen-plus", apiKey: KEY, temperature: 0, configuration: { baseURL: DASHSCOPE } });
const SYSTEM =
"你是「智扫通」扫地机器人的智能客服。" +
"产品相关问题必须调用 search_kb 查证后再回答,不要凭空作答;" +
"涉及用户机型/保修用 get_user_info;涉及清洁建议可用 get_weather。" +
"回答简洁、亲切。";
const agent = createAgent({
model,
tools: [searchKb, getWeather, getUserInfo],
prompt: SYSTEM,
middleware: [logging],
checkpointer: new MemorySaver(),
});
async function chat(text, user) {
const cfg = { configurable: { thread_id: user } };
const result = await agent.invoke({ messages: [{ role: "user", content: text }] }, cfg);
return result.messages[result.messages.length - 1].content;
}
console.log(await chat("我的滤网多久洗一次?", "u1"));
console.log(await chat("我是用户 U8899,我的机器还在保修吗?", "u1"));
console.log(await chat("那我刚才问的滤网,你还记得吗?", "u1")); // 记忆生效跑起来观察日志,你会看到 Agent 的决策过程:问滤网 → 调 search_kb;问保修 → 调 get_user_info;第三句考验记忆 → 它记得前面聊过滤网。三样能力 + 记忆,在一个 Agent 里协同工作。
接上界面
要变成能用的产品,把第八篇的界面接上即可:Streamlit 里把 st.session_state.chain 换成这个 agent、用 write_stream(agent.stream(...)) 流式展示;Node 那套 Express + HTML 里,把 /api/ask 的 ragChain.stream 换成 agent.stream。核心逻辑一行不用改——这就是分层的好处。
小结与预告
你完整搭出了一个生产雏形级的客服 Agent:RAG 作工具 + 多工具编排 + 中间件 + 记忆,并理解了「RAG 降格为 Agent 的一件工具」这个关键设计。到这里,从「调用一次模型」到「一个会查资料、会调工具、有记忆的智能体」,整条路你都亲手走通了。
最后一篇《结语与进阶方向》,我们回顾整个系列的知识地图,聊聊从 Demo 到生产还差哪些东西(评估、可观测、成本、安全),以及往下深入可以看什么。