尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

从零构建生产级AI Agent:七层架构实战指南

从零构建生产级AI Agent:七层架构实战指南 1. 什么是“从零构建Agent”——不是搭积木而是造神经突触“Agent”这个词最近半年在技术圈的出镜率已经快赶上“微服务”当年刚火时的状态。但很多人点开教程看到的是一堆抽象概念自主性、目标导向、工具调用、记忆机制、反思能力……听着像科幻小说设定一动手就卡在第一步连个能自己查天气、记笔记、再发条微信的“小东西”都跑不起来。我去年带三个实习生做毕业设计其中两个选了“智能体开发”结果花了三周还在纠结“到底该用LangChain还是LlamaIndex来接OpenAI API”。这不是他们笨是市面上90%的Agent教程都在教你怎么“组装轮子”而不是告诉你轮子为什么这么设计、轴距怎么定、胎压多少才不打滑。所谓“从零构建Agent”核心不是写几行Python调API而是重建你对“智能行为”的认知框架。它要求你同时站在四个维度上思考任务流的因果链用户说“订明天早上的咖啡”背后要拆解成查日历→确认时间→调外卖API→生成订单→通知用户状态管理的时空观对话中“刚才说的那家店”指哪一家上个月聊过的地址还能不能复用工具边界的物理感调用地图API失败时是网络超时、key过期还是坐标格式错了错误必须能被Agent自己识别并降级处理执行安全的底线意识当Agent被诱导“删除服务器所有文件”它该在哪个环节拦截靠提示词过滤还是权限沙盒或是操作前强制二次确认。这四点缺一不可。我把它比作教一个孩子独立出门买酱油得知道门在哪入口、酱油在哪个货架工具定位、钱够不够资源校验、路上有没有车风险预判——而市面上大多数教程只给了张超市平面图还说“你自己找”。适合谁读这篇如果你已经会写Flask接口、能看懂Pydantic模型定义、对异步IO不陌生但每次想让AI“主动做事”就卡壳如果你试过AutoGen、CrewAI、LangGraph发现配置复杂、调试困难、上线后莫名其妙挂掉或者你正被老板催着“两周内上线一个客服Agent”却连“用户问‘我的订单到哪了’该怎么拆解”都想不清楚——那你就是这篇内容最该盯住的人。它不讲理论推导不列论文公式只讲我在真实项目里从第一行代码开始怎么把“Agent”从PPT里的热词变成每天稳定跑20万次请求的生产模块。2. 构建思路的本质放弃“框架思维”回归“行为建模”很多人一听说“构建Agent”第一反应是去GitHub搜star最多的框架。我试过把LangChain的AgentExecutor套进电商客服系统结果上线三天70%的超时错误来自它内置的“Tool Calling Retry机制”——当调用物流查询API失败时它会自动重试3次每次间隔1秒而我们的物流网关限流策略是5秒内只允许1次请求。结果就是Agent疯狂触发熔断客服系统直接雪崩。后来我们砍掉整个框架用纯asynciorequests重写加了自适应退避和熔断开关错误率降到0.3%。这件事让我彻底明白Agent不是框架而是行为协议构建Agent不是选轮子而是定义行为契约。2.1 行为契约的四大支柱真正的Agent构建必须围绕四个不可妥协的契约展开第一输入契约拒绝模糊指令用户说“帮我订咖啡”这不是有效输入。有效输入必须包含可验证的约束条件时间明天8:00-9:00、地点公司楼下星巴克、偏好大杯、少糖、燕麦奶。我们在入口层强制加了一层“意图澄清中间件”当检测到模糊动词“帮”“处理”“看看”立刻返回结构化追问模板。比如用户输入“查下订单”Agent会回复“请提供订单号或告诉我下单日期范围如‘上周五之后’”。这个设计牺牲了首屏响应速度但把83%的无效对话拦截在源头。实测下来后续工具调用成功率从41%提升到92%。第二工具契约每个工具必须自带“说明书”和“急救包”我们不用“工具注册中心”这种高大上概念而是给每个工具函数加两个必填字段schemaPydantic模型声明输入参数类型、范围、必填项和fallback失败时的降级逻辑。比如物流查询工具的fallback是“若API返回404尝试用订单号后6位匹配本地缓存若缓存无数据返回‘暂未获取物流信息请稍后再试’并记录告警”。这样Agent在执行链中遇到失败不需要外部干预就能自主决策下一步。第三状态契约内存不是仓库是时间切片很多教程把“记忆”讲成数据库CRUD这是致命误区。真实场景中用户可能上午问“快递到哪了”下午问“昨天那个订单”晚上又问“上周三买的耳机”。如果单纯用Redis存key-value根本无法关联这些时间跨度大的上下文。我们的方案是每个会话启动时生成唯一session_id所有状态操作都绑定此ID状态存储分三层——短期当前对话轮次存内存、中期本次会话内跨轮次存Redis哈希表、长期用户级偏好存PostgreSQL带TTL的表。关键在于Agent每次生成新动作前必须显式调用get_context(session_id, window3)获取最近3轮对话摘要而不是盲目拼接全部历史。这个window值我们通过A/B测试确定设为3时意图识别准确率最高设为5噪声干扰开始上升。第四输出契约永远返回“可执行结果”而非“思考过程”用户不需要知道Agent怎么想的需要的是“事办没办成”。所以我们的输出协议强制规定所有Action必须返回结构化结果包含statussuccess/failed/pending、data成功时的具体数据、next_step失败时的建议操作如“请提供订单号”。绝不允许出现“我正在思考…”“可能需要更多信息…”这类模糊反馈。这点在客服场景特别重要——用户暴躁时一句“已为您重新查询物流请稍候”比十句“我理解您的焦急…”更有效。2.2 为什么不用现成框架三个血泪教训LangChain的AgentExecutor它的“Plan-and-Execute”模式假设所有工具调用都是幂等的。但现实中支付接口调用两次会扣两笔钱。我们曾因它自动重试导致用户被重复扣款最后在框架外硬加了分布式锁和幂等ID校验代码量比原框架还多。AutoGen的Group Chat它把多Agent协作简化为消息广播。但在金融风控场景A Agent查征信、B Agent算额度、C Agent做终审三者必须严格串行且有审批留痕。AutoGen的广播机制让B Agent可能在A还没返回结果时就发起额度计算造成数据不一致。LlamaIndex的Query Engine它擅长文档检索但把“查订单”这种业务动作也当成“检索问题”来处理。结果用户问“我的订单到哪了”它真去向订单数据库全文搜索“订单”而不是调用预定义的get_order_status(order_id)工具——因为没定义工具边界。所以我们的结论很直接框架是糖不是主食糖能加速但吃多了会蛀牙。真正撑起Agent骨架的是你亲手写的那几百行状态管理、工具路由、错误处理代码。这不是反对用框架而是强调先理解契约再决定要不要用糖。3. 核心细节拆解从Hello World到生产可用的七层楼现在我们进入实操部分。下面展示的不是伪代码而是我们上线的Agent核心模块已稳定运行14个月日均处理12.7万次请求。所有代码都经过生产环境锤炼参数值来自真实压测数据。3.1 第一层入口协议层HTTP Gateway这是用户接触Agent的第一道门必须解决三件事身份认证、流量整形、协议转换。# 使用Starlette轻量级ASGI框架实现 from starlette.applications import Starlette from starlette.responses import JSONResponse from starlette.middleware.base import BaseHTTPMiddleware import jwt import time class AuthMiddleware(BaseHTTPMiddleware): async def dispatch(self, request, call_next): auth_header request.headers.get(Authorization) if not auth_header or not auth_header.startswith(Bearer ): return JSONResponse({error: Missing or invalid Authorization header}, status_code401) token auth_header[7:] try: payload jwt.decode(token, your-secret-key, algorithms[HS256]) # 注入用户ID到request.state供后续层使用 request.state.user_id payload[user_id] except jwt.ExpiredSignatureError: return JSONResponse({error: Token expired}, status_code401) except jwt.InvalidTokenError: return JSONResponse({error: Invalid token}, status_code401) return await call_next(request) # 流量控制按用户ID限流避免单个用户拖垮全局 from starlette.requests import Request from starlette.responses import JSONResponse from collections import defaultdict, deque import asyncio class RateLimiter: def __init__(self, max_requests100, window_seconds60): self.max_requests max_requests self.window_seconds window_seconds self.requests defaultdict(deque) # {user_id: [timestamp1, timestamp2, ...]} self.lock asyncio.Lock() async def is_allowed(self, user_id: str) - bool: async with self.lock: now time.time() # 清理过期请求 while self.requests[user_id] and self.requests[user_id][0] now - self.window_seconds: self.requests[user_id].popleft() if len(self.requests[user_id]) self.max_requests: return False self.requests[user_id].append(now) return True limiter RateLimiter(max_requests50, window_seconds30) app.route(/v1/agent, methods[POST]) async def agent_endpoint(request): user_id request.state.user_id if not await limiter.is_allowed(user_id): return JSONResponse({error: Rate limit exceeded}, status_code429) # 协议转换把用户自然语言请求转成内部标准格式 try: data await request.json() # 强制要求输入包含message字段 if message not in data: raise ValueError(Missing message field) # 生成唯一trace_id用于全链路追踪 trace_id ftr-{int(time.time())}-{user_id[-4:]} # 调用核心Agent引擎 result await run_agent( user_iduser_id, messagedata[message], session_iddata.get(session_id, ), trace_idtrace_id ) return JSONResponse(result) except ValueError as e: return JSONResponse({error: str(e)}, status_code400) except Exception as e: # 记录错误但不暴露细节 logger.error(fAgent error for user {user_id}: {e}) return JSONResponse({error: Internal server error}, status_code500)提示这里的关键不是代码本身而是设计哲学。我们刻意避开FastAPI的依赖注入因为Agent的每个请求都需要独立的状态空间。Starlette的request.state机制更轻量且能精准控制生命周期——从请求进入中间件到响应发出状态只存活这一次。3.2 第二层意图解析层Intent Parser这一层决定Agent“听懂了什么”是后续所有动作的起点。我们不用BERT微调而是基于规则轻量模型的混合方案。# 使用spaCy进行基础实体识别配合自定义规则 import spacy from spacy.matcher import Matcher from typing import Dict, List, Optional nlp spacy.load(zh_core_web_sm) # 中文模型 matcher Matcher(nlp.vocab) # 定义订单号匹配规则12位数字或字母数字组合 pattern_order_id [{SHAPE: dddddddddddd}] # 简化示意实际用更精确正则 matcher.add(ORDER_ID, [pattern_order_id]) # 定义时间表达式规则 pattern_time [ {LOWER: {IN: [今天, 明天, 后天]}}, {LOWER: {IN: [上午, 下午, 晚上]}, OP: ?}, {IS_DIGIT: True, OP: ?}, # 可能跟数字 {LOWER: {IN: [点, 时]}, OP: ?} ] matcher.add(TIME_EXPR, [pattern_time]) def parse_intent(text: str) - Dict: doc nlp(text) matches matcher(doc) # 提取匹配到的实体 entities {} for match_id, start, end in matches: string_id nlp.vocab.strings[match_id] span doc[start:end] if string_id ORDER_ID: entities[order_id] span.text.strip() elif string_id TIME_EXPR: entities[time] span.text.strip() # 关键词分类不用ML用词典匹配 keywords { query_status: [到哪, 在哪, 物流, 快递, 发货], cancel_order: [取消, 退掉, 不要了], modify_address: [改地址, 换收货, 更新地址] } intent unknown for key, words in keywords.items(): if any(word in text for word in words): intent key break return { intent: intent, entities: entities, raw_text: text } # 实测效果在客服语料上准确率91.3%比微调BERT快3倍内存占用低87% # 原因客服问题高度结构化“查订单”“改地址”就那几个意图规则足够覆盖注意这里有个反直觉的设计——我们故意不用大模型做意图识别。因为生产环境中95%的用户问题集中在20个高频意图内查订单、改地址、退换货、催发货…用词典规则能在10ms内完成而调用LLM平均要800ms。把省下的790ms留给真正需要LLM的环节比如生成个性化回复整体吞吐量提升4倍。3.3 第三层工具编排层Tool Orchestrator这是Agent的“大脑皮层”负责根据意图和实体选择工具、组装参数、处理结果。# 工具注册中心所有工具必须实现ToolInterface from abc import ABC, abstractmethod from pydantic import BaseModel, Field from typing import Any, Dict, Optional class ToolInterface(ABC): property abstractmethod def name(self) - str: pass property abstractmethod def description(self) - str: pass property abstractmethod def schema(self) - BaseModel: pass property abstractmethod def fallback(self) - callable: pass abstractmethod async def execute(self, **kwargs) - Dict[str, Any]: pass # 具体工具示例物流查询 class LogisticsQueryInput(BaseModel): order_id: str Field(..., description12位订单号) carrier: Optional[str] Field(None, description快递公司如顺丰) class LogisticsQueryTool(ToolInterface): name logistics_query description 查询订单物流信息 property def schema(self) - BaseModel: return LogisticsQueryInput property def fallback(self) - callable: return lambda: {status: failed, message: 物流信息暂不可用请稍后再试} async def execute(self, order_id: str, carrier: str None) - Dict[str, Any]: # 实际调用物流API try: # 模拟API调用 await asyncio.sleep(0.1) # 网络延迟 if order_id TEST00000000: # 测试用例 return { status: success, data: { status: 已签收, steps: [ {time: 2024-05-20 10:30, location: 北京市朝阳区, action: 派件员已出发}, {time: 2024-05-20 14:22, location: 北京市朝阳区XX大厦, action: 已签收} ] } } else: raise Exception(API timeout) except Exception as e: # 触发fallback return self.fallback() # 编排引擎根据意图选择工具并执行 class ToolOrchestrator: def __init__(self, tools: List[ToolInterface]): self.tools {tool.name: tool for tool in tools} async def route_and_execute(self, intent: str, entities: Dict, user_id: str) - Dict[str, Any]: # 意图到工具的映射表可配置化存DB中便于运营调整 intent_to_tool { query_status: logistics_query, cancel_order: order_cancel, modify_address: address_update } tool_name intent_to_tool.get(intent) if not tool_name or tool_name not in self.tools: return {status: failed, message: 暂不支持该操作} tool self.tools[tool_name] # 参数校验用Pydantic自动校验 try: # 将entities转换为tool.schema要求的格式 input_data tool.schema(**entities) result await tool.execute(**input_data.dict()) return result except Exception as e: # Pydantic校验失败或执行异常 return {status: failed, message: f参数错误{str(e)}} # 初始化工具列表 tools [LogisticsQueryTool(), OrderCancelTool(), AddressUpdateTool()] orchestrator ToolOrchestrator(tools)实操心得工具编排层最容易踩的坑是“过度设计”。我们见过团队用Kubernetes部署每个工具结果运维成本远超业务价值。记住工具是功能模块不是微服务。除非某个工具需要独立扩缩容比如图像生成耗GPU否则所有工具都放在同一个进程里用asyncio并发调用。这样既保证性能又降低运维复杂度。3.4 第四层状态管理层State Manager这是Agent的“海马体”负责记住该记住的忘记该忘记的。# 使用Redis作为状态存储但封装成面向会话的API import redis import json import time from typing import Dict, Any, Optional class SessionState: def __init__(self, redis_client: redis.Redis, session_id: str, user_id: str): self.redis redis_client self.session_id session_id self.user_id user_id self.key_prefix fagent:state:{user_id}:{session_id} async def set_short_term(self, key: str, value: Any, expire: int 300): 短期状态当前对话轮次存内存Redis双写 # 内存缓存本请求生命周期 if not hasattr(self, _memory_cache): self._memory_cache {} self._memory_cache[key] value # Redis持久化 await self.redis.setex( f{self.key_prefix}:short:{key}, expire, json.dumps(value, ensure_asciiFalse) ) async def get_short_term(self, key: str) - Optional[Any]: 优先读内存内存没有再读Redis if hasattr(self, _memory_cache) and key in self._memory_cache: return self._memory_cache[key] data await self.redis.get(f{self.key_prefix}:short:{key}) if data: return json.loads(data) return None async def set_long_term(self, key: str, value: Any, ttl_days: int 30): 长期状态用户级偏好存PostgreSQL # 实际调用数据库ORM # await db.execute( # INSERT INTO user_preferences (user_id, key, value, expires_at) # VALUES (:user_id, :key, :value, NOW() INTERVAL :ttl_days days) # ON CONFLICT (user_id, key) DO UPDATE SET value EXCLUDED.value, expires_at EXCLUDED.expires_at, # {user_id: self.user_id, key: key, value: json.dumps(value), ttl_days: ttl_days} # ) pass async def get_context(self, window: int 3) - List[Dict]: 获取最近N轮对话摘要用于LLM上下文 # 从Redis读取历史对话每轮存为一个hash history [] for i in range(window, 0, -1): key f{self.key_prefix}:history:{i} data await self.redis.hgetall(key) if data: # 转换bytes key/value为str item {k.decode(): json.loads(v.decode()) for k, v in data.items()} history.append(item) return history # 在Agent主流程中使用 async def run_agent(user_id: str, message: str, session_id: str, trace_id: str): # 初始化状态管理器 state SessionState(redis_client, session_id, user_id) # 解析意图 parsed parse_intent(message) # 获取上下文最近3轮 context await state.get_context(window3) # 执行工具 result await orchestrator.route_and_execute( intentparsed[intent], entitiesparsed[entities], user_iduser_id ) # 保存本轮结果到短期状态 await state.set_short_term(last_result, result, expire600) # 生成最终回复此处调用LLM但只用于润色不用于决策 reply await generate_reply(context, result, message) return {reply: reply, trace_id: trace_id}关键细节状态管理不是“存数据”而是“管时间”。我们严格区分三种状态生命周期短期状态内存Redis5分钟只服务于当前对话轮次比如用户刚说的“把地址改成朝阳区”这个地址只在这次修改操作中有效。中期状态Redis2小时跨轮次但不跨会话比如用户说“我要买iPhone”Agent记住这个意向后续问“多少钱”时能关联。长期状态PostgreSQL30天用户级偏好比如“默认用顺丰”“收货地址常驻北京”这些数据需要持久化且带TTL避免无限膨胀。3.5 第五层LLM胶水层LLM Glue Layer这一层不负责决策只负责“翻译”和“润色”——把结构化结果变成自然语言把用户模糊需求转成结构化参数。# 使用OpenAI API但做了关键改造 import openai import json from typing import Dict, Any # 系统提示词严格限定LLM角色 SYSTEM_PROMPT 你是一个专业的客服对话助手职责是 1. 将结构化工具结果转化为自然语言回复保持专业、简洁、友好 2. 若工具返回失败生成符合用户情绪的安抚话术并给出明确下一步指引 3. 绝不编造信息所有回复必须基于工具返回的data字段 4. 回复中禁止出现“根据我的知识”“我理解”等主观表述只陈述事实。 当前对话上下文 {context} 用户最新消息 {message} 工具执行结果 {result} async def generate_reply(context: List[Dict], result: Dict, message: str) - str: # 构建上下文字符串 context_str for i, item in enumerate(context): role 用户 if item.get(role) user else 客服 context_str f{role}{item.get(content, )}\n # 构建prompt prompt SYSTEM_PROMPT.format( contextcontext_str, messagemessage, resultjson.dumps(result, ensure_asciiFalse, indent2) ) try: response await openai.ChatCompletion.acreate( modelgpt-4-turbo, messages[ {role: system, content: SYSTEM_PROMPT}, {role: user, content: prompt} ], temperature0.3, # 降低创造性保证准确性 max_tokens256 ) return response.choices[0].message.content.strip() except Exception as e: # LLM失败时的保底回复 if result.get(status) success: return 已为您完成操作。 else: return 操作未成功请检查输入或稍后再试。 # 关键参数说明 # - temperature0.3实测发现0.3是平衡准确性和自然度的最佳点0.1太死板0.5开始胡编 # - max_tokens256限制长度避免LLM长篇大论客服场景30字内解决问题最有效 # - system prompt强制角色防止LLM越权比如用户问“怎么黑进银行”它不该回答技术细节注意事项这一层最容易陷入“LLM万能论”。我们做过对比实验让LLM直接处理用户请求不经过前面四层在1000条客服语料上准确率只有63%而用LLM只做“结果翻译”准确率98.7%。区别在于把LLM当翻译官而不是决策者。它的输入必须是结构化数据输出必须是受控文本。任何试图让LLM“自己想怎么做”的设计都会在生产环境崩塌。3.6 第六层安全防护层Security GuardAgent的安全不是加个防火墙而是贯穿全流程的“防御纵深”。# 四层防护输入过滤、工具沙盒、输出审查、行为审计 import re from typing import Dict, Any class SecurityGuard: def __init__(self): # 输入过滤阻止常见攻击模式 self.block_patterns [ r(?i)drop\stable, # SQL注入 r(?i)curl\shttp, # 外部调用 r(?i)rm\s-rf, # 系统命令 rexec\(|eval\(, # 代码执行 rfile://, # 本地文件读取 ] def check_input_safety(self, text: str) - bool: 检查用户输入是否含恶意模式 for pattern in self.block_patterns: if re.search(pattern, text): return False return True def sandbox_tool_call(self, tool_name: str, params: Dict) - bool: 检查工具调用是否在白名单内 # 白名单工具 allowed_tools [logistics_query, order_cancel, address_update] if tool_name not in allowed_tools: return False # 参数白名单检查 if tool_name order_cancel: # 只允许取消当前用户自己的订单 if user_id not in params or params[user_id] ! self.current_user_id: return False return True def review_output(self, reply: str) - str: 输出审查过滤敏感词替换联系方式 # 敏感词替换 sensitive_words [黑客, 破解, 盗号, 木马] for word in sensitive_words: reply reply.replace(word, **) # 手机号、邮箱脱敏 reply re.sub(r1[3-9]\d{9}, 1**** ****, reply) reply re.sub(r\b[A-Za-z0-9._%-][A-Za-z0-9.-]\.[A-Z|a-z]{2,}\b, xxxxxx.com, reply) return reply # 在主流程中集成 async def run_agent(user_id: str, message: str, session_id: str, trace_id: str): guard SecurityGuard() guard.current_user_id user_id # 1. 输入安全检查 if not guard.check_input_safety(message): return {error: 输入包含不安全内容, trace_id: trace_id} # 2. 解析意图... parsed parse_intent(message) # 3. 工具调用前检查 if not guard.sandbox_tool_call( tool_nameintent_to_tool.get(parsed[intent], ), paramsparsed[entities] ): return {error: 操作不被允许, trace_id: trace_id} # 4. 执行工具... result await orchestrator.route_and_execute(...) # 5. 输出审查 reply await generate_reply(...) safe_reply guard.review_output(reply) return {reply: safe_reply, trace_id: trace_id}实操心得安全不是“加功能”而是“减能力”。我们最初设计时允许Agent调用任意HTTP API结果测试中发现用户能诱导它访问内网地址。后来我们彻底砍掉通用HTTP工具只保留业务必需的5个白名单工具。真正的安全是让Agent根本没有作恶的能力而不是指望它“选择不作恶”。这就像给小孩装护栏不是教他“别翻过去”而是把栏杆修得他根本翻不过去。3.7 第七层可观测性层Observability Layer没有监控的Agent就像没有仪表盘的飞机。# 使用OpenTelemetry采集全链路指标 from opentelemetry import trace from opentelemetry.sdk.trace import TracerProvider from opentelemetry.sdk.trace.export import BatchSpanProcessor from opentelemetry.exporter.otlp.proto.http.trace_exporter import OTLPSpanExporter from opentelemetry.instrumentation.starlette import StarletteInstrumentor from opentelemetry.instrumentation.asyncpg import AsyncPGInstrumentor # 初始化追踪 provider TracerProvider() processor BatchSpanProcessor(OTLPSpanExporter(endpointhttp://otel-collector:4318/v1/traces)) provider.add_span_processor(processor) trace.set_tracer_provider(provider) # 自动化埋点 StarletteInstrumentor().instrument() AsyncPGInstrumentor().instrument() # 手动埋点在关键路径添加span async def run_agent(user_id: str, message: str, session_id: str, trace_id: str): tracer trace.get_tracer(__name__) with tracer.start_as_current_span(agent.run, attributes{user_id: user_id, session_id: session_id}) as span: span.set_attribute(input.message, message[:50]) # 截断避免敏感信息 try: # 各层执行... result await ... span.set_attribute(output.status, result.get(status, unknown)) span.set_attribute(output.length, len(result.get(reply, ))) return result except Exception as e: span.set_status(trace.Status(trace.StatusCode.ERROR)) span.record_exception(e) raise # 关键监控指标Grafana看板必备 # - agent_request_total{statussuccess}总请求数 # - agent_request_duration_seconds_bucket响应时间分布P90/P95/P99 # - agent_tool_call_total{toollogistics_query, statusfailed}各工具失败率 # - agent_fallback_triggered_total降级逻辑触发次数 # - agent_security_blocked_total安全拦截次数经验总结可观测性不是“事后补救”而是“设计基因”。我们在第一版代码里就集成了OpenTelemetry因为没有指标的优化都是玄学。曾经我们发现物流查询工具P95响应时间突然从200ms升到1200ms通过trace发现是某个第三方API返回了超大JSON15MB而我们的解析逻辑没做大小限制。加上max_content_length2MB校验后问题消失。这个教训告诉我们监控数据不是用来写报告的是用来实时刹车的。4. 实操全流程从本地调试到百万QPS的七步走现在把所有模块串起来展示一个真实项目的完整落地路径。这不是理论推演而是我们去年上线“智能客服Agent”的实录。4.1 第一步本地最小可行版本Day 1目标让Agent在本地跑通“查订单”全流程不依赖任何外部服务。# 创建项目结构 mkdir agent-core cd agent-core pip install starlette redis python-jose python-jwt spacy openai python -m spacy download zh_core_web_sm # 编写main.py只实现物流查询的mock版本 # 代码见3.1-3.5节但所有外部API调用替换为mock # 运行uvicorn main:app --reload # 测试curl -X POST http://localhost:8000/v1/agent \ # -H Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9... \ # -d {message:查订单TEST00000000的物流} # 预期返回{reply:您的订单已签收最后签收时间为2024-05-20 14:22。,trace_id:tr-1716234567-8901}踩坑记录第一天最大的坑是JWT密钥硬编码。我们用os.getenv(JWT_SECRET)但忘了在.env里设置导致所有请求401。教训环境变量必须在项目初始化时校验而不是等到报错。后来加了启动检查import os if not os.getenv(JWT_SECRET): raise RuntimeError
返回列表