FastAPI与LangChain构建智能商品搜索服务

基于 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 v2Rust 内核,类型即文档
Agent 引擎LangChain 1.x + LangGraphReAct 循环 + 工具调用
会话记忆LangGraph Checkpointerthread_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 的概念迁移成本很低:

FastAPISpring Boot说明
APIRouter@Controller路由定义
BaseModelDTO数据传输对象
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_caseCamelCastUtils 自动转驼峰,两端无需手动转换。

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.gettime.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 费用,两种治理策略:

  1. 消息窗口截断:只保留最近 N 条消息
  2. 摘要节点:把旧历史压缩成 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 侧当作一个独立的、无状态的微服务节点来设计:

  1. FastAPI 提供与 Java 侧对齐的 REST 接口和统一响应格式
  2. LangGraph Agent 负责"理解需求 → 检索知识 → 结构化推荐"的完整推理链路
  3. Checkpointerthread_id 管理会话归属,服务端本身无状态,水平扩展友好

这套方案改造成本低,AI 能力独立成服务可快速迭代,不影响 Java 主链路稳定性。


如果本文对你有帮助,欢迎点赞收藏。有问题欢迎评论区讨论!

关键词FastAPILangChainLangGraphAgentPydantic v2智能搜索RAG微服务

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值