AI Agent四层技术栈:从大模型底座到工具调用的工业级落地
1. 这不是概念图,是能直接照着搭环境的四层技术栈地图
你打开任何一篇讲AI Agent的文章,十有八九会看到一张漂亮的分层架构图:最底下是“大模型”,中间是“推理引擎”,上面是“记忆/规划”,顶上是“工具调用”。但问题来了——这张图没法让你在终端里敲出第一行能跑起来的代码。它像一张城市鸟瞰图,告诉你有CBD、有住宅区、有地铁线,可你站在街角,还是不知道该往哪条巷子拐才能找到那家能调试Agent的咖啡馆。
我做AI工程落地三年,从用LangChain写第一个天气查询Bot,到给制造业客户部署带设备控制API的工业级Agent系统,踩过所有层的坑。今天这篇不画饼、不堆术语,就干一件事:把“02_AI Agent的4层技术栈”从PPT概念,还原成你明天上午就能在本地Mac或Windows上拉起一个可交互、可调试、可替换组件的最小可行系统。核心关键词全在标题里:AI Agent、技术栈、大模型、工具调用、底层架构——它们不是并列名词,而是存在强依赖关系的四层漏斗:上层能力必须建立在下层稳定交付的基础上。
比如你刚学会用Python写个@tool装饰器,以为工具调用就搞定了,结果发现模型根本不会触发它;你换了个更强的开源大模型,却发现之前的提示词全失效了;你按教程配好了向量数据库做记忆,一跑长对话就OOM……这些都不是“配置错误”,而是四层之间没对齐的典型症状。这篇文章要解决的,就是帮你建立一套“层间对齐检查清单”:当你在某一层做改动时,立刻知道需要同步验证哪几处下层接口、哪些上层参数、哪些中间状态。它不教你“什么是RAG”,而是告诉你当RAG模块返回空结果时,该先查向量库的embedding维度是否和模型输出对齐,还是先看检索query是否被LLM预处理阶段意外截断。全文所有描述、所有命令、所有参数,都来自我过去18个月在真实项目中反复验证过的最小可运行组合——不是实验室玩具,是能扛住每秒30次并发请求的工业级底座。
2. 四层技术栈的本质:不是分层,而是责任隔离与故障域划分
2.1 第一层:大模型底座——不是“选哪个模型”,而是“定义你的计算契约”
很多人把“大模型底座”理解为选一个HuggingFace上的模型权重文件,比如Qwen2-7B-Instruct或Phi-3-mini-4k-instruct。这是最大的认知偏差。底座真正的职责,是为你整个Agent系统定义计算契约(Computational Contract):它承诺以确定的输入格式接收数据,以确定的输出格式返回结构化响应,并在确定的延迟和成本约束下完成计算。
举个具体例子:你用Ollama本地部署llama3:8b,它的契约是:
- 输入:纯文本prompt,最大上下文4K tokens
- 输出:流式text,需自行解析JSON块
- 延迟:P95 < 1.2s(M2 Ultra实测)
- 成本:单次推理约0.8GB显存占用
而如果你换成vLLM部署的Qwen2-7B,契约就变了:
- 输入:需构造
messages数组,支持tool_choice字段 - 输出:原生返回
{"choices": [{"message": {"tool_calls": [...]}}]}结构 - 延迟:P95 < 0.4s(A10G实测)
- 成本:单次推理显存占用降至0.5GB,但需额外管理vLLM的engine进程
提示:契约不匹配是90%的Agent故障根源。比如你用LangChain的
ChatPromptTemplate给vLLM发请求,却没在system_message里声明工具schema,模型就会静默忽略工具调用指令——它没“错”,只是严格履行了“不识别未声明工具”的契约。
所以选底座的第一步,不是比参数,而是画一张契约对齐表:
| 能力需求 | Ollama+llama3 | vLLM+Qwen2 | LlamaFactory微调版 |
|---|---|---|---|
| 原生工具调用支持 | ❌ 需手动解析JSON | ✅tool_calls字段 | ✅ 可定制tool schema |
| 流式输出结构化 | ❌ 纯文本 | ✅ 带token计数的JSON | ✅ 可注入结构化标记 |
| 低成本本地部署 | ✅ M系列Mac直跑 | ⚠️ 需GPU服务器 | ❌ 至少2×A10G |
| 微调后工具泛化能力 | ⚠️ 需重训全量 | ✅ LoRA高效适配 | ✅ 全参数微调 |
我当前主力项目用的是vLLM+Qwen2组合,因为客户要求“工具调用失败时必须返回标准错误码而非自由发挥”,这只有原生支持tool schema的模型才能保证。而个人学习项目用Ollama,因为MacBook Pro M3 Max跑phi-3-mini足够快,且ollama run phi3一行命令就能启动,省去所有容器编排。
2.2 第二层:推理引擎——不是“调用API”,而是“构建可控的决策流水线”
把大模型当黑盒API调用,是Agent开发最危险的起点。第二层推理引擎的核心任务,是把一次用户请求,拆解成可审计、可中断、可重试的原子决策步骤。它不是简单的“发prompt→收response”,而是要管理四个关键状态:
- 意图识别态:判断用户当前请求属于哪个技能域(如“查订单”vs“改地址”)
- 工具规划态:决定调用哪些工具、以什么顺序、传什么参数
- 执行协调态:并发调用多个工具,处理超时/失败/降级
- 响应合成态:把工具返回的原始数据,转化为自然语言回答
以Java生态的LangChain4j为例,它的引擎设计暴露了这个本质。看这段真实生产代码:
// 定义工具链:不是简单注册方法,而是声明输入/输出契约 Tool weatherTool = Tool.builder() .name("get_weather") .description("获取指定城市的实时天气,输入为city_name字符串") .method(WeatherService::getCurrentWeather) // 绑定具体实现 .build(); // 构建可审计的决策流水线 AiServices aiServices = AiServices.builder() .chatLanguageModel(model) // 绑定第一层底座 .tools(weatherTool, orderTool) // 注册第二层工具集 .toolExecutor(ParallelToolExecutor.builder() // 关键!声明执行策略 .maxConcurrentCalls(3) // 最大并发数 .timeout(Duration.ofSeconds(15)) // 单工具超时 .build()) .build();注意ParallelToolExecutor——它不是“让工具跑得更快”,而是定义故障域边界:当天气API超时,订单API仍能正常返回,引擎会自动降级,只用订单数据生成部分回答。这种设计让整个Agent具备了传统Web服务才有的SLA保障能力。
Python生态的LlamaIndex则走了另一条路:用QueryEngine抽象层统一调度。它的优势在于天然支持RAG,但代价是调试复杂度陡增。我在一个金融客服项目中发现,当用户问“上季度我的基金收益是多少”,LlamaIndex默认会先检索知识库再调用工具,但实际需要先调用基金账户API获取持仓,再用持仓代码去查收益——这要求手动覆盖QueryEngine的默认路由逻辑。最终我们用自定义RouterQueryEngine重写了路由规则,把“含账户ID的查询”全部导向工具层。
注意:所有主流框架(LangChain/LlamaIndex/Flowise)的“链式调用”本质都是语法糖。真正决定Agent鲁棒性的,是你在引擎层显式声明的状态转移规则。比如LangChain的
RunnableWithFallbacks,表面是“主流程失败走备用”,底层其实是注册了一个状态监听器,在on_chain_error事件中触发降级逻辑。
2.3 第三层:记忆与状态管理——不是“存聊天记录”,而是“维护跨会话的业务上下文”
把Agent的记忆等同于“把历史消息拼进prompt”,是新手最容易掉进的坑。第三层真正的挑战,是如何在无状态的HTTP请求和有状态的业务流程之间架桥。比如电商场景中,用户说“把刚才看的那款手机加入购物车”,这里的“刚才看的”指向一个具体的SKU,但HTTP协议本身不保存这个关联。
我们团队的解决方案是分三级记忆:
短期记忆(Session Memory):用Redis Hash存储单次会话的临时状态
key:session:{uuid}
field:last_viewed_sku,cart_items,user_intent
TTL: 30分钟(防内存泄漏)中期记忆(User Memory):用PostgreSQL的JSONB字段存用户偏好
table:users
column:profile→{"preferred_payment": "alipay", "shipping_address_id": 123}
更新时机:用户明确设置偏好时长期记忆(Knowledge Memory):用ChromaDB向量库存业务知识
collection:product_knowledge
embedding: 使用与大模型一致的text-embedding-3-small
检索策略:混合检索(keyword + vector),避免纯语义检索导致的SKU错位
关键技巧:所有记忆读写必须通过统一的MemoryService门面。这样当用户说“对比iPhone15和华为Mate60”,系统能自动从last_viewed_sku取出两个SKU,再调用比价工具——而不是让每个工具自己去猜用户意图。
曾有个严重Bug:用户在微信小程序里点击商品详情页,前端没传sku_id参数,后端MemoryService检测到last_viewed_sku为空,就触发了默认兜底逻辑,把用户最近购买的SKU当成了“刚才看的”。修复方案很简单:在MemoryService的get方法里加一行日志,记录每次读取的key和来源(HTTP header / cookie / URL param),三天内就定位到是小程序SDK版本升级导致参数丢失。
2.4 第四层:工具调用层——不是“写API接口”,而是“定义可组合的原子能力”
第四层常被误解为“把公司内部API包装成函数”。但真正的工具层设计,要遵循三个工业级原则:
- 幂等性原则:同一工具调用多次,结果必须一致(如
get_user_profile),或明确声明副作用(如create_order需带唯一request_id) - 契约一致性原则:所有工具的输入/输出必须符合统一Schema,我们强制使用OpenAPI 3.0规范生成SDK
- 可观测性原则:每个工具调用必须返回
{status, duration_ms, error_code, trace_id}标准头
看一个真实工具定义(Spring Boot + OpenAPI):
# openapi.yaml /components: schemas: WeatherResponse: type: object properties: city: type: string temperature: type: number format: double condition: type: string ToolError: type: object properties: code: type: string enum: [SERVICE_UNAVAILABLE, INVALID_PARAM, RATE_LIMIT_EXCEEDED] message: type: string responses: WeatherSuccess: description: 天气查询成功 content: application/json: schema: $ref: '#/components/schemas/WeatherResponse' ToolError: description: 工具调用错误 content: application/json: schema: $ref: '#/components/schemas/ToolError'生成的Java SDK里,WeatherService.getCurrentWeather()方法签名强制包含@ApiResponse(responseCode = "200")和@ApiResponse(responseCode = "429"),确保LLM在规划时能准确预判失败场景。
最反直觉的经验:不要试图让LLM“理解”工具功能,而是让它“记住”工具契约。我们在system prompt里固定插入一段:
你可用的工具列表(按调用频率排序): 1. get_weather(city: str) → {temperature: float, condition: str} 【重要】仅当用户明确询问天气时调用,不用于推测用户所在地 2. create_order(items: list[dict], address_id: int) → {order_id: str, status: str} 【重要】必须传入address_id,不可用默认地址 ...这段文字占prompt约120 tokens,但让工具调用准确率从73%提升到91%。因为LLM不是在“推理”该用哪个工具,而是在“模式匹配”——它把用户query和工具描述里的关键词(如“天气”“temperature”)做向量相似度计算,比纯逻辑推理更稳定。
3. 实操:从零搭建可调试的四层Agent(Mac/Linux环境)
3.1 环境准备:用Docker Compose一键拉起全栈
放弃手动安装各种依赖。我们用Docker Compose定义四层服务的最小耦合关系:
# docker-compose.yml version: '3.8' services: # 第一层:vLLM大模型底座(Qwen2-7B) vllm: image: vllm/vllm-openai:latest command: > --model Qwen/Qwen2-7B-Instruct --tensor-parallel-size 1 --dtype half --enable-chunked-prefill --max-num-batched-tokens 8192 --port 8000 ports: - "8000:8000" deploy: resources: limits: memory: 12G devices: - driver: nvidia count: 1 capabilities: [gpu] # 第二层:LangChain4j推理引擎(Java服务) engine: build: ./engine ports: - "8080:8080" environment: - VLLM_BASE_URL=http://vllm:8000/v1 depends_on: - vllm # 第三层:Redis记忆服务 redis: image: redis:7-alpine command: redis-server --save 60 1 --loglevel warning ports: - "6379:6379" # 第四层:模拟工具服务(Python FastAPI) tools: build: ./tools ports: - "8001:8001" environment: - REDIS_URL=redis://redis:6379/0 depends_on: - redis关键设计点:
- 所有服务通过Docker网络通信,避免localhost端口冲突
vllm服务暴露标准OpenAI API格式(/v1/chat/completions),让LangChain4j无需修改即可接入engine服务启动时自动检测VLLM_BASE_URL可用性,超时3次则退出,防止僵尸进程
实操心得:第一次运行
docker-compose up -d后,务必执行docker-compose logs -f vllm确认模型加载完成。常见失败原因是GPU显存不足——vLLM默认会尝试占用所有GPU内存,需用--gpu-memory-utilization 0.8参数限制。
3.2 第一层实操:用Ollama快速验证底座(无GPU环境)
如果你没有NVIDIA GPU,用Ollama是最优解。以下命令在Mac M系列芯片上实测通过:
# 1. 安装Ollama(官网下载pkg,或brew install ollama) # 2. 拉取轻量模型(phi-3-mini仅2.2GB,M3 Max 12秒加载完成) ollama pull phi3 # 3. 启动服务并测试基础能力 ollama serve & curl http://localhost:11434/api/chat -d '{ "model": "phi3", "messages": [{"role": "user", "content": "你好,请用中文回答"}], "stream": false }' | jq '.message.content' # 4. 关键验证:测试工具调用能力(phi3原生支持) curl http://localhost:11434/api/chat -d '{ "model": "phi3", "messages": [ {"role": "system", "content": "你是一个天气助手,可用工具:get_weather(city: str) -> {temperature: int, condition: str}"}, {"role": "user", "content": "北京现在多少度?"} ], "tools": [{"type": "function", "function": {"name": "get_weather", "parameters": {"city": "string"}}}], "stream": false }' | jq '.message.tool_calls'如果最后一步返回null,说明模型未激活工具调用模式。此时需更新Ollama模型文件:
- 创建
Modelfile:
FROM phi3 SYSTEM """ 你是一个严格遵守工具契约的助手。当用户请求涉及工具时,必须返回tool_calls字段。 """- 构建新模型:
ollama create my-phi3 -f Modelfile - 用
my-phi3替代原模型测试
这个过程揭示了底座层的核心事实:工具调用能力不是模型固有属性,而是由system prompt+模型微调共同决定的契约。
3.3 第二层实操:LangChain4j引擎的可调试配置
在./engine/src/main/java/com/example/agent/AgentConfig.java中,我们定义了可热更新的引擎配置:
@Configuration public class AgentConfig { @Bean @ConfigurationProperties(prefix = "agent.engine") public AgentEngineProperties agentEngineProperties() { return new AgentEngineProperties(); } @Bean public ChatLanguageModel chatLanguageModel( @Value("${vllm.base-url}") String baseUrl, AgentEngineProperties props) { // 关键:启用结构化输出,强制LLM返回JSON return OpenAiChatModel.builder() .baseUrl(baseUrl) .apiKey("no-key-needed-for-vllm") .logRequests(true) // 开启请求日志,调试必备 .logResponses(true) .temperature(props.getTemperature()) // 可动态调整 .maxTokens(props.getMaxTokens()) .build(); } @Bean public AiServices aiServices(ChatLanguageModel model, List<Tool> tools) { return AiServices.builder() .chatLanguageModel(model) .tools(tools.toArray(new Tool[0])) .toolExecutor(ParallelToolExecutor.builder() .maxConcurrentCalls(2) // 生产环境设为3,调试时设为1便于跟踪 .timeout(Duration.ofSeconds(8)) .build()) .build(); } }启动后访问http://localhost:8080/actuator/env,可实时查看所有配置项。当发现工具调用失败时,立即检查/actuator/loggers将com.example.agent日志级别设为DEBUG,所有prompt、tool_calls、执行结果都会打印到控制台。
注意:
logRequests=true会产生大量日志,生产环境必须关闭。但我们保留了logRequestHeaders=true,只记录HTTP头中的X-Request-ID,既满足审计要求,又不拖慢性能。
3.4 第三层实操:Redis记忆的会话隔离实现
在./engine/src/main/java/com/example/memory/SessionMemoryService.java中,我们实现了严格的会话隔离:
@Service public class SessionMemoryService { private final RedisTemplate<String, Object> redisTemplate; // 用ThreadLocal存储当前会话ID,避免在异步调用中丢失上下文 private static final ThreadLocal<String> currentSessionId = ThreadLocal.withInitial(() -> UUID.randomUUID().toString()); public void setLastViewedSku(String sku) { String key = "session:" + currentSessionId.get(); redisTemplate.opsForHash().put(key, "last_viewed_sku", sku); redisTemplate.expire(key, Duration.ofMinutes(30)); } public String getLastViewedSku() { String key = "session:" + currentSessionId.get(); Object sku = redisTemplate.opsForHash().get(key, "last_viewed_sku"); return sku != null ? (String) sku : null; } // 关键:在HTTP请求进入时绑定会话ID @Component public static class SessionIdFilter implements Filter { @Override public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { HttpServletRequest httpRequest = (HttpServletRequest) request; String sessionId = httpRequest.getHeader("X-Session-ID"); if (sessionId == null || sessionId.isEmpty()) { sessionId = UUID.randomUUID().toString(); } currentSessionId.set(sessionId); try { chain.doFilter(request, response); } finally { currentSessionId.remove(); // 必须清理,防内存泄漏 } } } }这个设计解决了微信小程序场景下的经典问题:用户从公众号菜单进入,前端未携带session ID,后端自动生成并返回给前端存储。下次请求带上该ID,就能延续购物车状态。
3.5 第四层实操:工具服务的契约验证测试
在./tools/src/test/java/com/example/tools/WeatherToolTest.java中,我们编写了契约验证测试:
@SpringBootTest class WeatherToolTest { @Test void should_return_valid_weather_response() { // Given WeatherRequest request = new WeatherRequest("Beijing"); // When WeatherResponse response = weatherService.getCurrentWeather(request); // Then assertThat(response.getTemperature()).isBetween(-50, 50); // 温度合理性校验 assertThat(response.getCondition()).isNotEmpty(); assertThat(response.getCity()).isEqualTo("Beijing"); } @Test void should_throw_exception_for_invalid_city() { // Given WeatherRequest request = new WeatherRequest("NonExistentCity123"); // When & Then assertThatThrownBy(() -> weatherService.getCurrentWeather(request)) .isInstanceOf(ToolException.class) .hasFieldOrPropertyWithValue("errorCode", "CITY_NOT_FOUND"); } }所有工具必须通过此类测试才能上线。我们用GitHub Actions配置了CI流水线:
mvn test运行单元测试curl -X POST http://tools:8001/openapi.json验证OpenAPI文档生成python -m pytest tests/integration/运行端到端集成测试(调用真实天气API)
当某次提交导致should_throw_exception_for_invalid_city测试失败,CI会直接阻断发布,并在PR评论中贴出错误日志——这比人工Code Review快10倍。
4. 故障排查:四层技术栈的典型问题速查表
4.1 第一层故障:大模型底座异常
| 现象 | 根本原因 | 排查命令 | 解决方案 |
|---|---|---|---|
curl http://localhost:8000/v1/chat/completions返回503 | vLLM未完成模型加载 | docker logs vllm | grep "Starting OpenAI API server" | 等待日志出现INFO: Uvicorn running on http://0.0.0.0:8000 |
| 模型返回乱码或空响应 | GPU显存不足触发OOM | nvidia-smi | grep "Memory-Usage" | 降低--max-num-batched-tokens值,或换用量化模型 |
| 工具调用始终不触发 | system prompt未声明工具schema | curl ... -d '{"messages":[{"role":"system","content":"..."}]}' | 在system prompt中添加可用工具:get_weather(city: str)等明文描述 |
| P95延迟>2s | CPU瓶颈(vLLM默认用CPU解码) | htop | grep vllm | 添加--enforce-eager参数强制GPU解码 |
实操心得:在vLLM启动命令中加入
--disable-log-stats,否则日志刷屏掩盖关键错误。我们用--log-level WARNING把日志降到最低必要级别。
4.2 第二层故障:推理引擎决策失灵
| 现象 | 根本原因 | 日志特征 | 解决方案 |
|---|---|---|---|
aiServices.chat("北京天气")返回普通文本而非tool_calls | LangChain4j未正确注册工具 | 日志中无ToolExecutor invoked字样 | 检查AiServices.builder().tools(...)是否传入非空工具列表 |
| 工具调用超时但无降级响应 | ParallelToolExecutor配置错误 | 日志中出现TimeoutException但无fallback日志 | 在AiServices.builder()中添加.fallbacks(List.of(fallbackHandler)) |
| 同一prompt多次调用返回不同结果 | temperature参数过高 | 日志中temperature=0.8 | 生产环境设为0.3,调试时临时调高 |
| 内存持续增长直至OOM | ThreadLocal未清理 | 日志中OutOfMemoryError: Java heap space | 在SessionIdFilter的finally块中确保currentSessionId.remove() |
关键技巧:在application.properties中配置logging.level.com.langchain4j=DEBUG,可看到完整的prompt组装过程。当发现prompt里缺失工具描述时,立即检查SystemMessage是否被其他拦截器覆盖。
4.3 第三层故障:记忆服务状态错乱
| 现象 | 根本原因 | 快速验证 | 解决方案 |
|---|---|---|---|
| 用户A的操作影响用户B的购物车 | Redis key未加用户前缀 | redis-cli KEYS "session:*"查看key数量 | 改用session:{user_id}:{session_id}复合key |
| 会话30分钟后仍能获取数据 | TTL未生效 | redis-cli TTL session:abc123返回-1 | 检查redisTemplate.expire()调用位置,确保在put之后 |
last_viewed_sku偶尔为null | 多线程竞争写入 | redis-cli MONITOR | grep "last_viewed_sku" | 改用redisTemplate.opsForHash().putIfAbsent()原子操作 |
| 向量检索返回无关结果 | embedding模型与大模型不一致 | curl http://vllm:8000/v1/embeddings -d '{"input":"test"}' | 确保ChromaDB使用的embedding模型与vLLM完全相同 |
注意:在Redis中执行
KEYS *会阻塞服务,生产环境必须用SCAN 0 MATCH "session:*" COUNT 1000分批扫描。
4.4 第四层故障:工具调用失败
| 现象 | 根本原因 | 排查路径 | 解决方案 |
|---|---|---|---|
get_weather返回{"error":"service unavailable"} | 天气API限流 | curl -I https://api.weather.com/v3/weather/forecast/daily | 在工具服务中实现指数退避重试,首次失败后等待1s再试 |
| 工具返回JSON但LLM无法解析 | 字段名大小写不匹配 | 对比OpenAPI.yaml中temperaturevs 实际返回Temperature | 在工具服务中添加DTO转换层,统一转为snake_case |
| 并发调用时数据库连接池耗尽 | HikariCP配置过小 | curl http://tools:8001/actuator/metrics/hikaricp.connections.active | 将spring.datasource.hikari.maximum-pool-size从10调至20 |
| 工具调用成功但LLM忽略结果 | tool_call_id不匹配 | 日志中tool_call_id="call_abc"但response中id="call_def" | 在工具服务返回体中严格复用请求中的tool_call_id字段 |
终极排查法:在工具服务入口添加@EventListener(ApplicationReadyEvent.class),启动时打印所有已注册工具的OpenAPI路径,确保/openapi.json能被引擎服务正常访问。
5. 工业级落地的关键经验:四层协同的3个生死线
5.1 生死线一:工具调用的“三明治日志”必须贯穿四层
很多团队只在引擎层打日志,导致问题定位像考古。我们必须实现端到端trace ID透传,形成“三明治日志”:
- 顶层(用户请求):HTTP Header中注入
X-Request-ID: req_abc123 - 中层(引擎决策):在
AiServices调用前后打印[req_abc123] Planning tools: [get_weather] - 底层(工具执行):工具服务收到请求时记录
[req_abc123] Calling get_weather for Beijing
这样当用户投诉“查天气没反应”,运维只需在Kibana中搜索req_abc123,就能看到完整链路:[req_abc123] Engine sent tool_call to get_weather[req_abc123] Tools service received get_weather request[req_abc123] Tools service returned {"temperature":25,"condition":"sunny"}[req_abc123] Engine synthesized response: "北京现在25度,晴天"
没有这个能力,任何“高可用Agent”都是空中楼阁。
5.2 生死线二:大模型底座的“降级开关”必须物理隔离
当vLLM服务宕机,不能让整个Agent不可用。我们的方案是:
- 在引擎层实现
FallbackChatLanguageModel,当vLLM超时,自动切换到Ollama的phi3轻量模型 - 在API网关层配置熔断器(Resilience4j),连续5次失败后开启熔断,后续请求直接走降级模型
- 最关键:降级模型必须使用完全相同的prompt模板和tool schema,确保输出格式一致
这意味着你要提前准备好两套模型——不是“备用”,而是“主备同构”。我们在application.yml中配置:
agent: engine: fallback: enabled: true model: phi3 base-url: http://ollama:11434当vLLM恢复时,熔断器自动半开,放行部分流量验证,全部成功后才关闭熔断。这个机制让我们在去年一次GPU服务器故障中,保持了99.2%的API可用性。
5.3 生死线三:工具契约的“变更双签”制度
工具接口变更(如天气API增加湿度字段)必须触发双重验证:
- 技术侧:OpenAPI文档更新后,自动生成SDK并运行所有契约测试
- 产品侧:PM在Jira中创建
TOOL_CONTRACT_CHANGE任务,必须附上LLM调用该工具的10个典型prompt样本,由算法工程师验证变更后这些prompt是否仍能正确触发工具
我们曾因跳过产品侧验证,导致一个get_stock工具新增warehouse_id必填参数后,LLM仍按旧schema调用,返回400 Bad Request。修复方案不是改代码,而是让PM提供20个真实用户query,算法团队用这些query训练了一个小型分类器,自动检测何时需要传warehouse_id——这比硬编码规则更鲁棒。
最后分享一个血泪教训:在微信小程序上线前,我们发现iOS端WebView对fetch的keepalive支持不佳,导致长会话中X-Session-ID丢失。解决方案不是改前端,而是在引擎层增加SessionRecoveryFilter:当检测到无session ID时,自动从用户最近3次请求中提取sku_id,用它作为会话ID的种子生成新ID。这个补丁上线后,iOS端会话中断率从12%降至0.3%。
这些不是教科书里的理论,是我在凌晨三点盯着监控面板时,用咖啡和键盘敲出来的生存法则。AI Agent的四层技术栈,从来不是静态的架构图,而是动态的战场——每一层都在对抗不确定性,而你的工作,就是用工程手段把这种不确定性,压缩到业务可接受的范围内。
