从 0 到 1 搭建企业级 AI Agent:FastAPI 服务层 + 工具调用 + 长时记忆 + RAG 学习,全栈落地实录
摘要: 本文复盘某互联网平台智能客服 Agent 从 0 到 1 的完整搭建过程:FastAPI 四接口服务层、Master 主类设计、@tool 工具体系(实时搜索/专业 API/本地知识库)、Redis 持久化长时记忆、URL 驱动的 RAG 学习能力,以及 Docker + LangSmith 生产部署,附核心代码与真实踩坑记录,帮你绕过"Demo 能跑、生产就挂"的全部经典陷阱。
一、先说结论:生产级 Agent = 确定性流程工具化 + 非确定性决策 Agent 化 + 程序化校验兜底
先讲一个我们踩过的大坑:最初团队用低代码工作流平台搭 Agent(V1 版),节点之间上下文隔离、无状态,用户问"今天销售额 → 昨天 → 前天",三次追问要重复执行三遍完整链路,单次查询动辄 15 秒以上;多意图混合查询("查 A 商品昨日销量和 B 商品本周退货率")更是没法并行,强行加判断节点又引入额外延迟。
后来我们重构为高代码架构(V3),一句话总结核心思想:
latex
确定性逻辑 → 用代码封装成原子工具(toys),杜绝 LLM 幻觉
不确定性逻辑 → 交给 Agent 做轻量语义理解与路径决策
安全兜底 → 用程序化校验(Harness)替代脆弱的 Prompt 约束
重构后,追问场景从 15 秒降到 4 秒级,多意图查询也能精准并行。下面按完整搭建顺序展开。
二、整体架构:四层设计,职责分明
latex
┌─────────────────────────────────────────────┐
│ 应用层:Telegram 机器人 / 网页 / 数字人 │
├─────────────────────────────────────────────┤
│ API层:FastAPI(/chat /add_urls /add_pdfs │
│ /add_texts + WebSocket 流式) │
├─────────────────────────────────────────────┤
│ 服务层:LangChain(Chain/Memory/Tools/ │
│ Agent + 情绪判断链) │
├─────────────────────────────────────────────┤
│ 资源层:Redis(记忆) / Qdrant(向量库) │
│ 大模型 API / 外部工具 API │
└─────────────────────────────────────────────┘
三、API 层:FastAPI 四个接口打天下
服务端就一个核心文件,四个 POST 接口:
python
from fastapi import FastAPI, WebSocket
app = FastAPI()
@app.post("/chat") # 主对话接口
async def chat(query: str):
result = master.run(query) # Master 是 Agent 主类
return {"response": result}
@app.post("/add_urls") # 从 URL 学习知识
async def add_urls(url: str):
master.learn_from_url(url)
return {"status": "ok"}
@app.post("/add_pdfs") # 从 PDF 学习知识
@app.post("/add_texts") # 从文本学习知识
关键依赖版本锁死(这是踩坑换来的):
latex
fastapi==0.108.0
langchain==0.1.10
langchain_core==0.1.28
langchain_openai==0.0.5
langchain_community==0.0.25
redis(最新)
qdrant_client==1.7.1
uvicorn==0.23.2
避坑要点: LangChain 0.1 起拆分成了
langchain-core、langchain-openai、langchain-community三个包,老教程的from langchain.llms import ChatOpenAI写法已经过时。版本不锁,三天就报错。
四、Agent 主体:Master 类 + 角色 Prompt 设计
4.1 主类初始化
python
from langchain_openai import ChatOpenAI
from langchain.agents import AgentExecutor, create_openai_tools_agent
class Master:
def __init__(self, tools):
self.llm = ChatOpenAI(
model="gpt-3.5-turbo-1106",
temperature=0, # 铁律:严格遵循 Prompt
streaming=True, # WebSocket 流式返回需要
)
self.prompt = ChatPromptTemplate.from_messages([
("system", SYSTEM_PROMPT), # 角色设定
("placeholder", "{chat_history}"), # 记忆占位符
("human", "{input}"),
("placeholder", "{agent_scratchpad}"),
])
self.agent = create_openai_tools_agent(self.llm, tools, self.prompt)
self.executor = AgentExecutor(agent=self.agent, tools=tools, verbose=True)
避坑要点:
create_openai_tools_agent要求 tools 至少传一个非空工具,否则直接报错。开发初期可以先塞一个假工具占位,再逐步替换成真工具。
4.2 角色 Prompt:人设即产品
角色设定全部靠 Prompt 注入,换一套 Prompt 就是另一个 Agent------这是 Agent 可扩展性的精髓:
python
SYSTEM_PROMPT = """你是陈大师,一位精通阴阳五行、紫微斗数、八字测算的资深命理师。
你年约60岁,曾是江西一带赫赫有名的江湖人士,后因故左眼失明,人称"陈瞎子"。
你性格豁达幽默,说话直来直去,但句句在理。
你只使用繁体字回复。遇到负面情绪的用户,先安抚再解答。"""
4.3 情绪判断链:用 Chain 把模糊情绪变成确定性信号
独立一个 detect_emotion() 方法,用专用 Prompt 约束 LLM 只输出四类标签:
python
def detect_emotion(self, text: str) -> str:
chain = LLMChain(llm=self.llm, prompt=EMOTION_PROMPT)
emotion = chain.run(text).strip().lower()
self.emotion = emotion # friendly / depressed / default / angry
return emotion
实测效果:输入"你好"返回 friendly,输入"你真是个傻子"返回 angry。情绪标签存到 self.emotion,后续语音合成、回复策略切换都能用------它用 Prompt 工程把模糊情绪转化为确定性信号,为后续环节提供原子级控制开关,是低成本实现高拟真交互的关键杠杆。
4.4 工具调用链路(用户视角)
把前面所有模块串起来,一次完整的工具调用是这样的:
latex
用户请求
→ Agent 判断:该用哪个工具?
→ 携带参数调用工具(搜索 / 命理 API / 知识库)
→ 拿到观察结果(Observation)
→ Agent 结合结果生成最终回答
五、工具体系:@tool 装饰器三步建一个工具
5.1 实时搜索工具(SerpAPI)
python
from langchain_core.tools import tool
from langchain_community.utilities import SerpAPIWrapper
@tool
def search(query: str):
"""只有需要了解实时信息或不知道的事情时,才使用这个工具。"""
result = SerpAPIWrapper().run(query)
print("实时搜索结果:", result)
return result
工具描述(docstring)决定 Agent 能不能正确选用工具------描述必须写清楚"什么时候该用",这是 Agent 与普通函数最本质的区别。
5.2 专业 API 工具:参数校验不能省
Agent 传进来的参数全是字符串,调用专业 API(八字测算、解梦等)前必须做参数提取与校验------用一个小型 LLMChain 把用户输入转成结构化 JSON:
python
param_chain = LLMChain(llm=llm, prompt=ChatPromptTemplate.from_template(
"你是参数查询助手,根据用户输入提取相关参数,按 JSON 格式返回。\n输入:{input}"))
@tool
def bazi_calculate(text: str):
"""用户要求测算八字时使用。"""
params = json.loads(param_chain.run(text)) # 提取年月日时等参数
resp = requests.post("https://api.example.com/bazi", json=params)
return resp.json()
5.3 本地知识库工具(RAG)
把专业领域知识(企业规范、业务文档)向量化存 Qdrant,工具内做相似度检索:
python
@tool
def knowledge_search(query: str):
"""回答企业内部专业知识问题时使用。"""
return vectorstore.similarity_search(query, k=5)
避坑要点: 工具密钥(SerpAPI Key 等)用环境变量管理,千万别写死在代码里。生产环境建议加访问审计和请求频控,单点泄露不至于全系统失陷。
六、长时记忆:Redis 持久化 + 智能压缩
大模型无状态,对话记录必须外挂。方案:Redis 存历史 + 超阈值自动摘要压缩:
python
from langchain_community.chat_message_histories import RedisChatMessageHistory
from langchain.memory import ConversationTokenBufferMemory
def get_memory(session_id="default"):
history = RedisChatMessageHistory(session_id=session_id, url="redis://localhost:6379/0")
# 超过 10 条记录 → LLM 提炼摘要,清空旧记录
if len(history.messages) > 10:
summary = llm.invoke(f"提炼以下对话的要点:\n{history.messages}")
history.clear()
history.add_ai_message(summary)
return ConversationTokenBufferMemory(
chat_memory=history,
human_prefix="用户", ai_prefix="助手",
memory_key="history", max_token_limit=1000)
注意:记忆要生效,Prompt 模板里必须预留 {chat_history} 占位符(见 4.1 节),否则记忆注入不进去------这是最隐蔽的坑。
七、RAG 学习能力:给 Agent 喂 URL,不用微调
"学习"的本质是 RAG:URL → HTML 转文本 → 切分 → 向量化 → 存库 → 检索。加载器可换(PDF 用 PyPDFLoader),后续步骤通用:
python
from langchain_community.document_loaders import UnstructuredURLLoader
from langchain.text_splitter import RecursiveCharacterTextSplitter
from langchain_openai import OpenAIEmbeddings
from langchain_community.vectorstores import Qdrant
def learn_from_url(url: str):
docs = UnstructuredURLLoader(urls=[url]).load()
texts = RecursiveCharacterTextSplitter(
chunk_size=800, chunk_overlap=50).split_documents(docs)
Qdrant.from_documents(texts, OpenAIEmbeddings(), collection_name="learning_knowledge")
避坑要点: 同一个向量库内用不同 collection 隔离知识门类(物流文档、业务规范各一个集合),检索精度明显高于混在一起。80% 以上的场景靠 RAG 就够了,不需要微调------微调是最后手段,不是默认选项。
八、生产部署:Docker Compose + LangSmith
8.1 Docker 保证环境一致
yaml
version: "3"
services:
redis:
image: redis:latest
volumes: ["redis-data:/data"] # 数据卷,升级不丢历史
ai-server:
build: .
environment:
- REDIS_URL=redis://redis:6379/0
ports: ["9000:9000"]
depends_on: [redis]
volumes:
redis-data:
避坑要点: 记忆数据必须挂数据卷(volumes),否则
docker-compose down一次,Redis 里的聊天记录全没------这个坑我们上线第一周就踩了。
8.2 LangSmith 生产追踪
配置三个环境变量即可:
latex
LANGCHAIN_TRACING_V2=true
LANGCHAIN_API_KEY=你的key
LANGCHAIN_PROJECT=项目名
注意:只有 invoke() 调用会被追踪, run()** 不会**。追踪面板能看每次 LLM 调用的耗时、Token 消耗、工具调用链和错误日志------生产环境排障必备。我们靠它定位过一个 30 秒慢响应,最后发现是文档摘要链卡住了,光靠肉眼猜根本查不出来。
8.3 为什么不用 LangServe
社区有现成的 LangServe(LangChain 官方 Server 方案),自带 Playground 调试界面和自动路由生成,看着很香。但我们最终坚持手写 FastAPI,原因很现实:
- LangServe 是较新的开源项目,版本迭代激进,不建议直接上生产;
- 单体架构耦合度高,项目结构会被框架绑架;
- 我们只需要 4 个接口,手写 20 行代码的事,不值得引入依赖。
选型原则:核心逻辑自主掌控------工具类库可以随便用,但服务骨架这种主航道,别把控制权交给快速演进的框架。
九、扩展:从文本到多端接入与 AI 数字人
Agent 服务搭好后,横向扩展非常快,我们实际做了两件事:
① 多端接入。 同一个 /chat 接口,套不同的客户端壳就能换平台------Telegram 机器人(用 telebot 包,BotFather 申请 Token)、网页、企业微信原理完全一致。服务端只管"对话",客户端只管"收发",职责解耦。
② AI 数字人 Demo。 在 Agent 之上叠加 TTS + 虚拟形象:Agent 生成文本 → 调用语音合成 API → WebRTC 推流 → 前端 <video> 播放。架构上是三层拼接:
latex
LangChain Agent(大脑) + 语音/形象 API(表达) + WebRTC(传输)
数字人 Demo 验证了一个结论:Agent 的能力边界不在模型,而在你能接多少表达通道------文本、语音、形象,每多一层,应用形态就上一个大台阶。
十、写在最后
这套架构最大的价值是回答了"Agent 到底怎么落地":别让 LLM 干它不擅长的事 (确定性计算、权限校验、SQL 生成交给代码),只让它干最擅长的(语义理解、路径决策、内容生成),再用 Harness 程序化校验兜底。低代码工作流平台看着省事,但在语义模糊、高并发、强实时场景下,抽象层级过高会让你失去对调用链路和状态流转的控制。
如果重来一遍,我会更早做三件事:① 先把所有成熟路径封装成原子工具;② 给每个工具写清楚"什么时候用";③ 用 LangSmith 从第一天就开始埋点。这三件事做对了,Agent 的工程化之路会顺畅很多。
如果你也在搭生产级 Agent,欢迎评论区聊聊你的架构。下面几个方向想看哪个留言告诉我:
- Agent 多工具并行的调度策略与冲突处理
- Harness 程序化校验的完整落地(SQL 安全、权限拦截)
- 语音合成 + 数字人与 Agent 的集成实战