LangGraph-handler
LangGraph 中间件中的 Handler 深度实践
结合生产项目,聊聊中间件的洋葱模型与 6 种实用模式
项目背景
我们构建了一个 AI 测试用例生成系统,基于 LangGraph + DeepAgent 框架。核心链路如下:
前端 → LangGraph API → Agent → 中间件链 → Tools → LLM
核心原理:洋葱模型
项目定义了 4 个中间件,按顺序嵌套执行:
agent = create_agent(
model=llm,
tools=get_all_tools(),
middleware=[
skills_middleware, # 1. 加载 Skills
dynamic_model_selection, # 2. 动态模型选择
RAGMiddleware(), # 3. RAG 工具控制
PDFContextMiddleware() # 4. PDF 处理
],
system_prompt=SYSTEM_PROMPT
)
执行流程(洋葱模型):
请求进入
│
▼
┌─────────────────────────┐
│ 1. SkillsMiddleware │
│ 加载 Skills → System Prompt
└─────────────────────────┘
│
▼
┌─────────────────────────┐
│ 2. dynamic_model_selection
│ 检测图片 → 切换模型 │
└─────────────────────────┘
│
▼
┌─────────────────────────┐
│ 3. RAGMiddleware │
│ 开关控制 → 过滤 RAG 工具
└─────────────────────────┘
│
▼
┌─────────────────────────┐
│ 4. PDFContextMiddleware │
│ 提取 PDF → 追加提示 │
└─────────────────────────┘
│
▼
LLM 推理(最内层)
每一层都可以在调用 handler 前后添加逻辑,这就是中间件的精髓。
6 种实战模式
模式1:标准调用(观察模式)
场景:日志、监控、条件开关。
示例:RAGMiddleware 控制 RAG 功能是否启用。
async def awrap_model_call(self, request, handler):
if self._is_rag_enabled(request):
request = self._inject_rag_prompt(request)
else:
request = self._filter_rag_tools(request)
return await handler(request) # 继续向下传递
模式2:动态模型选择(请求修改)
场景:根据输入内容切换不同模型(成本优化)。
项目中,检测到图片消息时自动切换到多模态模型(豆包 Vision),纯文本则使用 DeepSeek,节省约 80% 成本。
@wrap_model_call
async def dynamic_model_selection(request, handler):
if _has_image_in_messages(request):
model = image_llm_model # 豆包 Vision(贵)
else:
model = deepseek_model # DeepSeek(便宜)
return await handler(request.override(model=model))
图片检测逻辑:
def _has_image_in_messages(request):
for msg in request.messages:
content = msg.content
if isinstance(content, list):
for block in content:
if block.get("type") in ("image", "image_url"):
return True
return False
模式3:渐进式工具暴露(项目核心创新 ⭐)
场景:按需暴露工具,避免 Agent 误调用,减少 token 消耗。
我们注册了 6 个工具(Excel、PDF、RAG 检索等)。当 RAG 功能关闭时,直接将 RAG 相关工具从列表中过滤掉:
class RAGMiddleware(AgentMiddleware):
async def awrap_model_call(self, request, handler):
if self._is_rag_enabled(request):
return await handler(self._inject_rag_prompt(request))
else:
# 核心:过滤 RAG 工具
return await handler(self._filter_rag_tools(request))
def _filter_rag_tools(self, request):
filtered = [t for t in request.tools if not self._is_rag_tool(t)]
return request.override(tools=filtered)
效果:
- 启用 RAG → 工具列表包含
rag_query、rag_graph_search等 - 禁用 RAG → 仅保留
export_excel、extract_pdf等基础工具
模式4:多重调用(重试机制)
场景:HTTP 请求自动重试 + 指数退避。
项目中的 RAG 服务客户端实现:
async def _request(self, method, path, **kwargs):
for attempt in range(3): # 最多重试3次
try:
resp = await client.request(...)
return resp.json()
except httpx.HTTPStatusError as e:
if e.response.status_code == 401: # token 过期
await self._refresh_token()
continue
except (RequestError, TimeoutException):
pass
await asyncio.sleep(0.5 * (2 ** attempt)) # 0.5s, 1s, 2s
raise RAGAPIError("请求失败")
模式5:响应修改
场景:在模型返回后修改输出(如格式化、敏感词过滤)。
项目中的 PDFContextMiddleware 在前置阶段注入 PDF 信息,也可扩展后置逻辑:
async def awrap_model_call(self, request, handler):
# 前置:处理 PDF,修改请求
if pdf_info:
request = self._append_pdf_hint(request, pdf_info)
response = await handler(request) # 调用模型
# 后置:修改响应(可选)
response = self._filter_sensitive_words(response)
return response
模式6:模型降级(Fallback)
场景:主模型限流或出错时自动降级。可在 dynamic_model_selection 中扩展:
@wrap_model_call
async def model_with_fallback(request, handler):
try:
return await handler(request)
except RateLimitError:
# 降级到备用模型
return await handler(request.override(model=backup_model))
except Exception:
return await handler(request.override(model=free_model))
面试常见问答
Q:为什么这样设计中间件?
A:解决三个实际问题:
- 图片和文本成本差异 → 动态模型选择
- 无需 RAG 时仍暴露工具 → 渐进式工具过滤
- 用户上传 PDF 需要解析 → PDF 中间件
Q:中间件顺序重要吗?
A:非常重要。
- SkillsMiddleware(加载技能指令)
- DynamicModelSelection(确定模型)
- RAGMiddleware(处理 RAG 工具)
- PDFContextMiddleware(处理附件)
顺序决定了数据的流动方向,错误顺序会导致功能失效。
总结
| 模式 | 项目应用 | 核心作用 |
|---|---|---|
| 标准调用 | RAGMiddleware | 条件开关 |
| 请求修改 | dynamic_model_selection | 动态切换模型 |
| 渐进式工具暴露 | RAGMiddleware._filter_rag_tools | 按需加载工具 |
| 多重调用 | RAGServiceClient._request | HTTP 重试 |
| 响应修改 | PDFContextMiddleware | 后置格式化 |
| 模型降级 | 可扩展 | 容错处理 |
核心思想:通过 handler 获得执行流程的控制权,将非功能性需求(开关、过滤、重试、降级)与业务逻辑解耦。
浙公网安备 33010602011771号