基于 FastAPI + LangChain/LangGraph 构建智能商品搜索与推荐服务(附完整代码)
摘要:本文从一个真实微服务项目出发,详解如何用 FastAPI + Pydantic v2 搭建异步 AI 服务层,再用 LangChain + LangGraph 实现带工具调用的 Agent 推荐引擎和多轮会话记忆。覆盖架构设计、代码实现、踩坑经验与生产优化建议,适合想在 Java 微服务体系中嵌入 AI 能力的开发者参考。
前言
在电商场景中,“搜索"早已不只是关键词匹配——用户希望用自然语言描述需求(比如"2000-4000 元的小米手机”),系统就能精准推荐商品并给出理由。
本文分享一个 Python AI 微服务的完整实现方案,它作为 Java Spring Cloud 微服务集群中的独立 AI 节点(端口 9010),通过 OpenFeign 被 Java 侧直接调用。核心技术栈:
| 层次 | 技术选型 | 说明 |
|---|---|---|
| Web 框架 | FastAPI + Uvicorn | 原生异步,适配 LLM 长耗时调用 |
| 数据校验 | Pydantic v2 | Rust 内核,类型即文档 |
| Agent 引擎 | LangChain 1.x + LangGraph | ReAct 循环 + 工具调用 |
| 会话记忆 | LangGraph Checkpointer | thread_id 隔离多轮对话 |
| 向量检索 | Redis Stack | 商品 Embedding 相似度搜索 |
一、为什么选 FastAPI 做 AI 服务层?
1.1 与 LLM 的天然契合
LLM 推理动辄数十秒,传统同步框架(如 Flask)每个请求占一个线程,并发量一上去线程池就吃紧。FastAPI 基于 Starlette(ASGI 异步网关),原生支持 async/await,单进程可挂起大量并发等待请求而不占线程——与 LLM 长耗时调用是天作之合。
1.2 类型即文档即校验
函数签名标注类型后,FastAPI 自动完成参数解析、类型转换、422 错误响应,并生成 OpenAPI 文档(与 Java 侧 Knife4j 同源规范)。开发体验上比手写 Swagger 注解高效得多。
1.3 与 Java 侧的概念对照
如果你熟悉 Spring Boot,FastAPI 的概念迁移成本很低:
| FastAPI | Spring Boot | 说明 |
|---|---|---|
APIRouter | @Controller | 路由定义 |
BaseModel | DTO | 数据传输对象 |
Result[T] 泛型 | 统一响应体 | {code, msg, data} |
@app.exception_handler | @RestControllerAdvice | 全局异常处理 |
Depends() | @Autowired | 依赖注入 |
二、项目结构与核心配置
2.1 依赖声明
# pyproject.toml
dependencies = [
"fastapi>=0.112.0",
"uvicorn>=0.30.0",
"pydantic>=2.10.0",
"pydantic-settings>=2.5.0",
"python-dotenv>=1.0.0",
"langchain>=1.2.10",
"langchain-openai>=0.2.0",
"langgraph>=1.2.10",
"langgraph-checkpoint-redis>=0.2.0",
]
2.2 配置管理(pydantic-settings)
用 BaseSettings 从 .env 文件和环境变量装配配置,一行代码搞定:
from pydantic_settings import BaseSettings, SettingsConfigDict
class Settings(BaseSettings):
# 数据库
MYSQL_HOST: str
MYSQL_PORT: int
MYSQL_USER: str
MYSQL_PASSWORD: str
# Redis
REDIS_URL: str
INDEX_NAME: str
# Embedding 服务
EMBED_BASE_URL: str
EMBED_MODEL: str
# LLM
BASE_URL: str
OPEN_API_KEY: str
LLM_MODEL: str
model_config = SettingsConfigDict(
env_file=".env",
env_nested_delimiter="__",
extra="ignore"
)
踩坑提醒:Windows 下系统环境变量优先于
.env文件,如果系统里设了同名变量,.env中的值会被覆盖。
2.3 FastAPI 应用启动
from fastapi import FastAPI
from fastapi.responses import JSONResponse
app = FastAPI(title="商品智能搜索", version="1.0.0")
@app.exception_handler(Exception)
async def global_exception_handler(request, exc):
logger.exception(exc)
return JSONResponse(
Result(code=500, msg=f"服务器内部错误:{exc}").model_dump()
)
if __name__ == "__main__":
uvicorn.run(
"smart_search.main:app",
host="0.0.0.0",
port=9010,
reload=True
)
三、Pydantic v2 统一响应模型
3.1 泛型 Result 对齐 Java 侧
项目要求 Python 服务与 Java 服务的响应格式完全一致,用 Pydantic 泛型实现:
from typing import TypeVar, Generic
from pydantic import BaseModel
T = TypeVar("T")
class Result(BaseModel, Generic[T]):
code: int
msg: str
data: T | None = None
Java 侧经 OpenFeign 直连调用本服务,出入参的 snake_case 由 CamelCastUtils 自动转驼峰,两端无需手动转换。
3.2 业务模型定义
from pydantic import BaseModel, Field
class SearchCondition(BaseModel):
"""结构化搜索条件 — Field description 会被注入提示词"""
keyword: str | None = Field(None, description="商品关键词")
min_price: float = Field(default=0, description="最低价格")
max_price: float = Field(default=100000, description="最高价格")
class ProductRecommendResponse(BaseModel):
"""Agent 结构化输出契约"""
summary: str
product_list: list[GoodsInfo]
reason: list[str]
Pydantic v2 注意:用
model_dump()替代 v1 的dict(),用model_validate()替代parse_obj()。
四、路由设计:同步与异步的正确选择
from fastapi import APIRouter
router = APIRouter(prefix="/api/v1", tags=["商品智能搜索接口"])
# 同步探活 — 简单健康检查,无需异步
@router.get("/test")
def test():
return Result.success("ok")
# 同步全量向量化 — def 路由自动跑在线程池,不阻塞事件循环
@router.get("/sync")
def sync():
productVectorSyncService.load_sku_from_mysql()
# ...全量重建向量索引
# ★ 异步推荐 — LLM 链路全程 async,不阻塞
@router.get("/recommend")
async def recommend(query: str, thread_id: str = "0"):
return await searchService.recommend_product(query, thread_id)
# 异步条件提取
@router.get("/extract")
async def extract(query: str):
return await searchService.extract_search_condition(query)
关键原则:
async def路由内不能调用阻塞函数(如requests.get、time.sleep),否则会卡死事件循环- 同步重活(如全量数据重建)用
def路由,FastAPI 自动交给线程池执行 - LLM 调用链路全程走
async/await
五、LangChain Agent:让 LLM 从"会说"变"会做"
5.1 Agent 的核心思路
LLM 本身只会生成文本,Agent 让它在推理中自主决定"要不要调工具、调什么、拿结果继续想"。本项目使用 LangChain 1.x 的 create_agent 高层 API,底层是 LangGraph 的 ReAct 循环(Reason + Act):
用户 query
→ [model 节点] 读 system prompt + 消息历史
→ 决定调用 vector_search_tool(query)
→ [ToolNode] 执行 similarity_search(k=10) → 文档拼接回填消息流
→ [model 节点] 综合工具结果生成最终回答
→ response_format 强制解析为 ProductRecommendResponse 结构体
5.2 定义 Agent 工具
@tool 装饰器把普通函数变成 LLM 可调用的工具。docstring 是 LLM 判断何时使用此工具的唯一依据,务必写清楚:
from langchain_core.tools import tool
@tool
def vector_search_tool(query: str) -> str:
"""商品向量检索工具,获取相关商品资料。"""
docs = self.vector_store.similarity_search(query, k=10)
return "\n".join(
f"{doc.page_content} | meta:{doc.metadata}"
for doc in docs
)
最佳实践:工具函数内用
try/except返回"错误说明文本"而非抛异常,让 LLM 有机会改参数重试。
5.3 System Prompt 反幻觉约束
你是商城商品推荐助手:
- 严禁编造不存在的商品信息(反幻觉)
- 必须先调用 vector_search_tool 检索知识库获得上下文
- 只允许基于检索结果推荐并给出理由;
查不到则固定返回 summary="暂无相关信息", product_list=[], reason=[]
- 输出纯 JSON,禁止 markdown/```json/注释等附加文本;不得增删字段
5.4 Agent 装配与调用
from langgraph.prebuilt import create_agent
agent = create_agent(
model=self.llm, # ChatOpenAI 实例
tools=[self.vector_search_tool], # 工具列表
system_prompt=self.search_prompt, # RAG 约束提示词
checkpointer=self.checkpointer, # 会话记忆
response_format=ProductRecommendResponse, # 结构化输出双保险
)
response = await agent.ainvoke(
{"messages": query},
{"configurable": {"thread_id": thread_id}}, # 会话隔离键由 Java 侧传入
)
return response["structured_response"] # 已校验的 Pydantic 对象
结构化输出双保险:System Prompt 要求纯 JSON + response_format 做 Pydantic schema 校验修复,两条防线确保输出可靠。
六、多轮会话记忆(Checkpointer)
6.1 工作原理
LangGraph 在每轮对话后把完整消息状态存档,key 为 thread_id。同一个 thread_id 再次请求时,历史消息自动注入上下文——实现指代消解的连续追问。
6.2 多轮对话示例
# 第一轮
recommend("价格在5000元以内的高性能游戏手机", thread_id="1")
# → 返回 3 款游戏手机推荐
# 第二轮(同一个 thread_id)
recommend("哪一款续航更好?", thread_id="1")
# "哪一款" 的指代靠 checkpointer 存档的第一轮历史消解 ✓
6.3 当前实现与改进方向
当前使用 InMemorySaver(进程内存),重启即失忆、多副本不共享。生产环境应切换为 Redis Checkpointer:
from langgraph.checkpoint.redis import RedisSaver
checkpointer = RedisSaver.from_url(settings.REDIS_URL)
checkpointer.setup() # 自动建表
替换后,thread_id 不变则用户会话跨实例连续,支持水平扩展。
七、LCEL 条件提取链
除了 Agent 推荐,项目还有一个独立功能:从用户自然语言中提取结构化搜索条件。这里用的是 LCEL(LangChain Expression Language),比 Agent 更轻量:
from langchain_core.output_parsers import PydanticOutputParser
from langchain_core.prompts import ChatPromptTemplate
parser = PydanticOutputParser(pydantic_object=SearchCondition)
prompt = ChatPromptTemplate.from_messages([
("system", self.search_extract_prompt), # 含 few-shot 示例
("human", "用户查询:{query}"),
]).partial(format_instructions=parser.get_format_instructions())
# ★ 声明式管道:prompt → LLM → 解析器
extract_chain = prompt | self.llm | parser
result = await extract_chain.ainvoke({"query": query})
# "2000-4000元的小米手机"
# → SearchCondition(keyword='小米手机', min_price=2000, max_price=4000)
LCEL 三行成链,小任务独立可测可复用。
八、生产环境优化建议
8.1 SSE 流式输出(体验优先级最高)
当前方案的最大体验短板:用户发一次请求,最长要等 6 分钟才看到结果。用 SSE(Server-Sent Events)+ 打字机效果可以极大改善体感:
from fastapi.responses import StreamingResponse
@router.get("/recommend/stream")
async def recommend_stream(query: str, thread_id: str = "0"):
async def event_generator():
async for event in agent.astream_events(
{"messages": query},
{"configurable": {"thread_id": thread_id}},
version="v2"
):
if event["event"] == "on_chat_model_stream":
token = event["data"]["chunk"].content
yield f"data: {token}\n\n"
yield "data: [DONE]\n\n"
return StreamingResponse(
event_generator(),
media_type="text/event-stream"
)
8.2 Depends 依赖注入
from fastapi import Depends
# 数据库会话管理 — yield 后自动关闭
async def get_session():
session = SessionLocal()
try:
yield session
finally:
session.close()
# 鉴权依赖链
async def parse_token(token: str = Header(...)):
return decode_token(token)
# 路由签名声明即生效
@router.get("/recommend")
async def recommend(
query: str,
db=Depends(get_session),
current_user=Depends(parse_token)
):
...
# 测试时一行替换真实模型
app.dependency_overrides[get_llm] = mock_llm
8.3 中间件栈
from fastapi.middleware.cors import CORSMiddleware
from fastapi.middleware.gzip import GZipMiddleware
# CORS(替代手工响应头)
app.add_middleware(
CORSMiddleware,
allow_origins=["*"],
allow_methods=["*"],
allow_headers=["*"],
)
# GZip 压缩大 JSON
app.add_middleware(GZipMiddleware, minimum_size=1024)
8.4 生产部署形态
# 开发模式(reload 热更新,不能上生产)
uvicorn smart_search.main:app --reload --port 9010
# 生产模式:gunicorn 多 worker 利用多核
gunicorn smart_search.main:app \
-k uvicorn.workers.UvicornWorker \
-w 4 \
--bind 0.0.0.0:9010
# 更云原生的方式:容器内单进程,多副本由 K8s 扩缩容
8.5 记忆成本治理
长对话历史会线性膨胀 token 费用,两种治理策略:
- 消息窗口截断:只保留最近 N 条消息
- 摘要节点:把旧历史压缩成 summary 再拼接,checkpointer 里只存压缩态
8.6 可观测性
接入 LangSmith,一行环境变量开启全量追踪:
export LANGCHAIN_TRACING_V2=true
export LANGCHAIN_API_KEY="your-key"
每次 Agent 循环的工具选择、prompt/completion token、各节点耗时瀑布图一目了然,调 prompt 从盲调变有据可依。
九、踩坑总结
优点
| 维度 | 具体表现 |
|---|---|
| 异步 I/O | 扛 LLM 长耗时并发,不阻塞事件循环 |
| 自动文档 | 函数签名即 Swagger,与 Java 侧 OpenAPI 互通 |
| Pydantic v2 | 校验性能好,泛型 Result 跨语言契约统一 |
| Agent 装配 | create_agent 一行搞定 ReAct 循环 |
| 会话记忆 | checkpointer 把多轮状态管理简化为一个 thread_id |
| LCEL | 三行成链,小任务独立可测可复用 |
缺点与注意事项
| 问题 | 说明 | 改进方向 |
|---|---|---|
| InMemorySaver | 重启失忆、多副本不共享 | 切换 Redis Checkpointer(依赖已声明未接线) |
| Agent 延迟不可控 | 多轮工具调用 RT 增长,Java 侧被迫 6 分钟读超时 | SSE 流式输出缓解体感 |
| /sync 接口无鉴权 | 误触会打爆 embedding 配额 | 加鉴权 + 频率限制 |
| 全局异常丢分类 | 一切压成 500 | 按异常类型分级响应 |
| 条件提取未接入推荐 | extract 和 recommend 两条链路未打通 | 后续用 extract 结果做向量过滤 |
| async 路由禁阻塞 | 调阻塞函数会卡死事件循环 | 同步重活用 def 路由交给线程池 |
| .env 密钥管理 | .env 含密钥不入仓库 | .gitignore 强制排除 |
十、架构图总览
┌──────────────────────────────────────────────────────────┐
│ Java Spring Cloud │
│ ┌──────────────┐ OpenFeign ┌───────────────┐ │
│ │ mall-search │ ──────────────────→ │ AiPythonFeign│ │
│ │ (Controller)│ HTTP :9010 │ Client │ │
│ └──────────────┘ └───────────────┘ │
└──────────────────────────────────────────────────────────┘
│
▼
┌──────────────────────────────────────────────────────────┐
│ FastAPI AI 服务 (Python 3.13) │
│ │
│ ┌─────────┐ ┌──────────────┐ ┌─────────────────┐ │
│ │ Router │──→│ SearchService│──→│ LangGraph Agent │ │
│ │ /api/v1 │ │ │ │ (ReAct 循环) │ │
│ └─────────┘ │ recommend() │ │ │ │
│ │ extract() │ │ vector_search │ │
│ └──────────────┘ │ _tool │ │
│ └────────┬────────┘ │
│ │ │
│ ┌─────────────┐ ┌──────────────────┐ │ │
│ │ Checkpointer│ │ Pydantic Models │ │ │
│ │ (会话记忆) │ │ Result[T] / DTO │ │ │
│ └─────────────┘ └──────────────────┘ │ │
└────────────────────────────────────────────┼───────────┘
│
▼
┌─────────────────┐
│ Redis Stack │
│ (向量索引) │
└─────────────────┘
结语
在 Java 微服务体系中嵌入 Python AI 服务,关键是把 Python 侧当作一个独立的、无状态的微服务节点来设计:
- FastAPI 提供与 Java 侧对齐的 REST 接口和统一响应格式
- LangGraph Agent 负责"理解需求 → 检索知识 → 结构化推荐"的完整推理链路
- Checkpointer 用
thread_id管理会话归属,服务端本身无状态,水平扩展友好
这套方案改造成本低,AI 能力独立成服务可快速迭代,不影响 Java 主链路稳定性。
如果本文对你有帮助,欢迎点赞收藏。有问题欢迎评论区讨论!
关键词:FastAPI、LangChain、LangGraph、Agent、Pydantic v2、智能搜索、RAG、微服务

953

被折叠的 条评论
为什么被折叠?



