到这里,已经具备了一条完整 Agent 链路需要的核心能力:上下文记忆、工具调用和多轮执行。
但是要真正进入工程化阶段,还需要补上两类能力:一是执行过程的扩展与治理,例如日志、重试、权限校验、提示词动态调整;二是对外提供稳定调用入口,让 Agent 能被其他服务或应用接入。
前者对应 Middleware,后者对应服务化。
#一、Agent Middleware 的作用与适用场景
链的行为如果一直都写在核心逻辑,那么想加日志、重试、权限校验、提示词调整,都要直接修改 Agent 或者工具代码。功能一朵,主流程很快就会变乱。
中间件就是解决这个问题的,把这些模型横切逻辑从核心流程代码中拆分出来,挂到 Agent 执行过程中的特定位置。
在 LangChain 里,中间件可以在模型调用、工具调用等阶段前后介入,读取或者修改执行状态,也可以提前终止、重试或者替换部分行为,而不改 Agent 本身。
常见用途包括:
- 观测:日志、Tracing、调试;
- 调整:动态修改提示词、工具选择、模型输入和输出格式;
- 控制:重试、回退、提前终止、人工审批;
- 治理:限流、权限校验、敏感信息处理。
这些场景看起来不同,但代码层面做的都是同一件事,在 Agent 执行流程的某个节点前后插入一段自定义逻辑。
#二、Agent Middleware 的生命周期钩子
LangChain 提供了六个常用钩子。它们不是并列执行的六个步骤,而是分布在一次 Agent 执行流程的不同位置。
| 钩子 | 触发位置 |
|---|---|
before_agent | 一次 Agent 调用开始时 |
before_model | 每次模型调用之前 |
wrap_model_call | 包裹模型调用,可在调用前后介入 |
after_model | 每次模型调用之后 |
wrap_tool_call | 包裹工具调用,可在调用前后介入 |
after_agent | 整轮 Agent 执行结束时 |
一次典型调用大致是:
before_agent
↓
before_model
↓
wrap_model_call
↓
Model
↓
after_model
↓
如果需要工具
↓
wrap_tool_call
↓
Tool
↓
回到 before_model
↓
...
↓
after_agent
请求进入后先经过 before_agent,接着进入模型调用阶段。wrap_model_call 包住真正的模型调用,因此可以在调用前后执行自定义逻辑;模型返回后再经过 after_model。
如果模型需要调用工具,wrap_tool_call 会包住工具执行。工具结果返回后,Agent 会重新进入模型调用阶段,让模型继续判断下一步。
因此,只要一次 Agent 执行中发生了多轮模型调用,before_model、wrap_model_call 和 after_model 就可能重复触发;wrap_tool_call 则会随着工具调用次数触发。
而 before_agent 和 after_agent 面向的是整次 Agent 调用,通常各执行一次。
某些钩子如果不需要修改状态,可以直接返回 None,表示不对当前执行过程做额外变更。
#三、Middleware的内置能力与自定义实现
LangChain 已经提供了一批常用 Middleware,用来处理对话摘要、人工审批、调用限额、失败回退、敏感信息处理、工具筛选和重试等问题。下边表格列举了一些:
| 中间件 | 作用 |
|---|---|
SummarizationMiddleware | 对话历史接近上限时自动汇总 |
HumanInTheLoopMiddleware | 暂停执行,等人工审核工具调用 |
ModelCallLimitMiddleware | 限制模型调用次数,防止成本过高 |
ToolCallLimitMiddleware | 限制工具调用次数,可全局也可限定某个工具 |
ModelFallbackMiddleware | 主模型失败时自动回退到备用模型 |
PIIDetectionMiddleware | 检测并处理个人身份信息 |
ToDoListMiddleware | 为 Agent 配备任务规划与跟踪 |
LLMToolSelectorMiddleware | 调用主模型之前,先由 LLM 筛选相关工具 |
ToolRetryMiddleware | 工具调用失败时按指数退避自动重试 |
LLMToolEmulator | 用 LLM 模拟工具执行,用于测试 |
ContextEditingMiddleware | 通过精简或清除工具使用记录来管理上下文 |
ShellToolMiddleware | 向 Agent 公开持久化的 shell 会话 |
FilesystemFileSearchMiddleware | 提供对文件的 Glob 与 Grep 搜索工具 |
自定义 Middleware 通常有两种方式:
- 装饰器:适合只需要一个 Hook 的简单逻辑;
- 类:继承
AgentMiddleware,适合同时使用多个 Hook,或者需要在Middleware内维护配置和共享逻辑。
例如,消息脱敏需要每次在调用模型前处理输入,可以实现 before_model:
from langchain.agents.middleware import AgentMiddleware
class DesensitizedDataMiddleware(AgentMiddleware):
def before_model(self, state, runtime):
...
return {"messages": new_messages}
before_model 拿到当前 Agent 状态,在模型真正调用之前完成处理;如果不需要修改状态,也可以返回 None。
多个 Middleware 可以同时挂到 create_agent 的 Middleware 列表中。不同 Hook 会在各自的生命周期位置执行,而 warp_model_call、warp_tool_call 这类包装型 Hook 会形成嵌套调用。
#四、把 Runnable 暴露为 HTTP 服务
中间件解决的是 Agent 内部的执行扩展,服务化解决的是另一件事:让这条链能够被其他应用通过网络调用。
前面的 Chain 或 Agent 还只是进程内对象:本地可以 invoke,但前端、其他服务或其他进程还不能直接访问它。
LangServe 可以把一个 Runnable 挂到 FastAPI 上,对外暴露成 HTTP 接口:
from fastapi import FastAPI
from langserve import add_routes
chain = prompt_template | client | parser
app = FastAPI(title="LangChain Service", version="1.0")
add_routes(app, chain, path="/langChain")
if __name__ == "__main__":
import uvicorn
uvicorn.run(app, host="localhost", port=8000)
add_routes 会根据 Runnable 的输入输出结构自动注册一组接口,比如:
POST /langchain/invoke
POST /langchain/batch
POST /langchain/stream
...
同时还提供:
/langchain/playground/
/docs
其中 /playground/ 用来交互式测试 Runnable,/docs 是 FastAPI 自动生成的 OpenAPI 文档。
这里 FastAPI 和 LangServe 各负责一层:
FastAPI提供 Web 服务、路由、参数校验、依赖注入和 OpenAPI;LangServe负责把Runnable的invoke / batch / stream等能力映射成 HTTP 接口。
因此,Runnable 只负责业务执行,HTTP 层负责对外暴露。调用只需要遵守接口约定,不需要了解链内部是怎么实现的。
#五、总结
Middleware把横切逻辑从核心代码里拆出来。 六类 Hook 分布在 Agent 执行生命周期的不同位置,模型调用和工具调用相关的Hook 可能在一次执行中重复触发;- 服务化负责把
Runnable变成网络接口。FastAPI提供 Web 服务基础,LangServe通过add_routes把invoke / batch / stream等能力映射成 HTTP 接口; - 接口建立后,调用方不再需要了解链的内部实现。 前端、其他服务或其他语言的程序,都可以按照统一的 HTTP 协议调用。
到这里,这个系列已经走过五篇:从一次模型调用开始,把组件组合成链,接入自己的知识库,再让他具备记忆和工具调用能力,最后通过 Middleware 扩展执行过程,并把能力暴露成服务。
这五篇是一条连续的工程路径:会调用、会组合、会接知识库、会调用外部能力、会做为服务运行。
后一阶段始终是建立在前一阶段之上,最后落到的,仍然是一条可以被组合、扩展和部署的执行链。