1. 为什么“Agent 框架开发”不是写个提示词就能跑通的事
最近两周,我连续帮三个不同行业的团队做技术方案评审,发现一个高度一致的现象:所有人在第一次提需求时都说“我们要做一个AI Agent”,但当被问到“这个Agent要解决什么具体问题、在哪个环节替代人、失败时如何兜底”,80%的人会停顿超过五秒,然后开始翻手机里刚收藏的LangChain教程链接。这背后暴露的,不是技术能力问题,而是对“Agent框架”本质的严重误判——它根本不是LLM调用接口的语法糖,而是一套带状态、有边界、可观测、能回滚的软件工程体系。
我去年主导过一个银行智能投顾Agent项目,初期团队也以为“把RAG+Function Calling拼起来就完事了”。结果上线首周,客户投诉率飙升37%,原因不是模型答错,而是Agent在用户反复修改风险偏好后,把历史对话中已否决的基金产品又重新推荐了一遍。根因查到最后,是状态管理模块缺失:没有为每个用户会话维护独立的决策上下文快照,也没有定义“偏好变更”这一事件的原子性操作边界。这让我彻底意识到,真正的Agent框架开发,核心战场其实在LLM之外——在状态机设计、在工具编排协议、在异常传播路径、在可观测性埋点。它和Spring Boot开发一个CRUD后台的本质区别在于:后者处理的是确定性数据流,前者处理的是概率性认知流;后者失败可重试,前者失败可能引发连锁认知偏差。
所以当你看到热搜里“agent开发”“ai agent”“langchain4j”这些词扎堆出现时,要警惕一种幻觉:仿佛只要选对SDK,填好API Key,再抄几段Chain代码,就能交付生产级Agent。现实是残酷的——我统计过近半年接手的12个Agent项目,其中9个在POC阶段就卡在“无法稳定复现相同输入下的相同输出”,根源全出在框架层:有的没做Prompt版本灰度,有的未隔离工具调用超时熔断,有的连基础的Token消耗监控都没有。这就像造汽车不装刹车系统,再炫酷的引擎也上不了路。因此,本文不讲“如何用LangChain调用OpenAI”,而是聚焦在那些被90%教程刻意忽略的框架骨架:状态如何持久化、工具如何安全注册、错误如何分级捕获、效果如何量化归因。这些才是决定Agent能否从Demo走向真实业务的生死线。
2. Agent框架的四大支柱:状态、工具、流程、可观测性
市面上绝大多数Agent教程都陷入一个致命误区:把框架等同于“调用大模型的胶水代码”。这种认知导致开发者在项目中期必然遭遇“胶水失效”——当业务逻辑变复杂,胶水粘不住了。真正健壮的Agent框架必须建立在四个不可妥协的支柱之上,缺一不可。下面我用银行投顾项目的实际架构图(已脱敏)来具象化这四大支柱的协作关系:
2.1 状态管理:让Agent拥有“记忆”与“身份”的底层机制
很多人以为Agent的记忆就是Conversation History,这是最大的认知陷阱。真实业务中,Agent需要同时维护三类状态:
- 会话态(Session State):用户当前对话的临时上下文,如“用户刚说想买稳健型产品”,需支持TTL自动过期(我们设为15分钟),避免僵尸会话占用内存;
- 用户态(User State):跨会话的长期画像,如风险测评分数、持仓产品列表,必须通过加密ID关联到数据库,且每次读写都要加分布式锁防止并发冲突;
- 任务态(Task State):进行中任务的中间状态,如“基金筛选任务已执行步骤1/3”,这是实现断点续传的关键,我们用Redis Stream存储,每条消息包含task_id、step、payload、timestamp。
提示:切勿用LLM自身记忆替代状态管理。我们在压测中发现,当会话历史超过20轮,模型对早期关键约束(如“不要推荐QDII基金”)的遗忘率高达63%。必须把硬性约束(hard constraints)抽离为结构化状态字段,由框架层强制校验。
我们采用分层状态存储策略:高频读写的会话态用本地LRU Cache(最大1000条),用户态走MySQL分库分表(按user_id哈希),任务态用Redis Stream保证消息有序。关键设计在于状态同步时机——不是每次LLM调用后都刷库,而是在工具调用成功、用户显式确认、或会话超时时触发持久化。这避免了高频IO拖垮性能,实测将单次响应延迟从1.2s降至380ms。
2.2 工具注册与沙箱:让Agent“能做事”而非“瞎联想”
Agent的价值不在“说得多好”,而在“做得多准”。但直接让LLM调用任意API是灾难性的。我们设计了三层工具管控机制:
- 声明式注册:每个工具必须提供JSON Schema描述输入/输出/副作用,框架据此生成标准化调用协议。例如基金查询工具必须声明
{ "type": "fund_search", "params": { "risk_level": "string", "min_return": "number" } },LLM只能按此格式生成参数,杜绝字符串拼接漏洞; - 运行时沙箱:所有工具调用在独立Docker容器中执行,资源限制为CPU 0.2核、内存300MB,超时强制kill。曾有个天气工具因第三方API故障卡死,沙箱机制使其不影响其他工具调用;
- 权限分级:工具按敏感度分三级——L1(只读,如行情查询)、L2(写操作,如下单)、L3(资金操作,如转账)。LLM调用L2/L3工具前,必须先通过风控规则引擎(如“单日累计下单不超过5笔”)。
注意:工具返回结果必须带可信度标签。我们要求所有工具在response中嵌入
{"confidence": 0.92, "source": "internal_db_v3"}字段。当LLM生成答案时,框架层会自动过滤掉confidence<0.7的结果,并触发降级策略(如返回“暂无可靠数据”而非胡编)。
2.3 流程编排引擎:让Agent“懂步骤”而非“乱发挥”
纯LLM驱动的Agent像即兴演员,而业务需要的是导演。我们弃用LangChain的SequentialChain,自研轻量级流程引擎,核心是状态机+事件驱动:
- 每个Agent任务对应一个有限状态机(FSM),如“基金推荐”任务的状态包括:
INIT → RISK_ASSESS → PRODUCT_FILTER → COMPARISON → RECOMMEND; - 状态迁移由事件触发,事件来源有三类:LLM输出(如
{"event": "risk_assessed", "data": {"score": 75}})、工具回调(如{"event": "products_fetched", "data": [...]})、用户动作(如点击“换一批”按钮触发{"event": "refresh_products"}); - 关键创新是状态守卫(Guard):每个状态迁移前执行守卫函数。例如从
PRODUCT_FILTER到COMPARISON,守卫函数会检查products.length >= 3 && products[0].fee_rate < 0.015,不满足则阻断迁移并提示用户。
这套机制让我们彻底摆脱了“LLM自由发挥”的不可控性。上线后,任务完成率从68%提升至94%,因为每个环节都有明确的准入/准出标准,不再是黑盒。
2.4 可观测性体系:让Agent“可诊断”而非“猜原因”
没有可观测性的Agent就像没有仪表盘的飞机。我们构建了四层监控:
- 请求层:记录每次LLM调用的prompt、completion、token数、耗时、温度值,按
agent_id + user_id + task_type聚合; - 工具层:监控每个工具的调用次数、成功率、P95延迟、错误码分布(如基金查询的HTTP 429频次突增,立刻定位到配额不足);
- 状态层:追踪状态变更链路,可视化展示“用户A的风险测评分数如何影响后续3个状态节点”;
- 业务层:定义核心业务指标,如“推荐采纳率”(用户点击推荐产品的比例)、“人工接管率”(Agent主动转人工的占比),当采纳率<40%时自动触发Prompt优化工单。
最实用的功能是Trace回放:运营人员输入任意会话ID,即可完整回放该次交互中所有状态变更、工具调用、LLM输出及原始日志,排查问题时间从平均2小时缩短至11分钟。
3. 从零搭建生产级Agent框架:核心模块实操详解
现在我们进入最硬核的部分——如何亲手搭建一个具备上述四大支柱的Agent框架。这里不依赖任何现有SDK,全部基于Python 3.11+标准库和轻量级组件,确保你理解每一行代码的意图。整个框架最终打包后仅127KB,却支撑了日均200万次Agent调用。
3.1 状态管理模块:用SQLite+内存缓存实现低延迟高一致性
我们放弃Redis等外部依赖,选择SQLite作为主状态存储,原因很实在:银行环境对第三方组件审计极严,而SQLite是C标准库,无需额外部署。关键是如何解决SQLite的写锁瓶颈?答案是WAL模式+连接池+本地缓存。
# state_manager.py import sqlite3 import json from contextlib import contextmanager from typing import Dict, Any, Optional from threading import local class StateManager: def __init__(self, db_path: str = "agent_state.db"): self.db_path = db_path self._local = local() # 线程本地存储,避免连接共享 self._init_db() def _init_db(self): # 启用WAL模式,允许多读一写并发 with self._get_conn() as conn: conn.execute("PRAGMA journal_mode = WAL") conn.execute(""" CREATE TABLE IF NOT EXISTS user_state ( user_id TEXT PRIMARY KEY, state_json TEXT NOT NULL, updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, version INTEGER DEFAULT 0 ) """) @contextmanager def _get_conn(self): if not hasattr(self._local, 'conn'): self._local.conn = sqlite3.connect( self.db_path, check_same_thread=False, timeout=5.0 # 防止锁等待过久 ) yield self._local.conn def get_user_state(self, user_id: str) -> Dict[str, Any]: # 先查本地缓存(LRU) if hasattr(self._local, 'cache') and user_id in self._local.cache: return self._local.cache[user_id] # 再查DB with self._get_conn() as conn: cursor = conn.execute( "SELECT state_json FROM user_state WHERE user_id = ?", (user_id,) ) row = cursor.fetchone() if row: state = json.loads(row[0]) # 缓存到线程本地(最多100条) if not hasattr(self._local, 'cache'): self._local.cache = {} self._local.cache[user_id] = state return state return {} def update_user_state(self, user_id: str, new_state: Dict[str, Any]) -> bool: # 使用version字段实现乐观锁 with self._get_conn() as conn: cursor = conn.execute( "SELECT version FROM user_state WHERE user_id = ?", (user_id,) ) row = cursor.fetchone() current_version = row[0] if row else 0 try: conn.execute( "INSERT OR REPLACE INTO user_state (user_id, state_json, version) VALUES (?, ?, ?)", (user_id, json.dumps(new_state), current_version + 1) ) conn.commit() return True except sqlite3.IntegrityError: # 版本冲突,需重试 return False实操心得:SQLite在WAL模式下,读操作完全不阻塞写操作,这是我们能承受高并发的关键。但要注意——必须为每个线程分配独立连接(通过threading.local),否则会出现连接被其他线程关闭的诡异错误。我们在线上环境实测,单机SQLite可支撑3000 QPS的用户态读写,远超预期。
3.2 工具注册中心:用装饰器+Schema验证构建安全沙箱
工具注册的核心诉求是:既要让LLM能理解工具能力,又要防止恶意调用。我们采用“声明即契约”模式,所有工具必须用@tool装饰器注册,并强制提供JSON Schema。
# tool_registry.py import json import inspect from typing import Dict, Any, Callable, List from pydantic import BaseModel, ValidationError class ToolSchema(BaseModel): name: str description: str parameters: Dict[str, Any] returns: Dict[str, Any] class ToolRegistry: _tools: Dict[str, Dict[str, Any]] = {} @classmethod def register(cls, schema: ToolSchema) -> Callable: def decorator(func: Callable) -> Callable: # 验证函数签名与schema匹配 sig = inspect.signature(func) if set(sig.parameters.keys()) != set(schema.parameters.get('properties', {}).keys()): raise ValueError(f"Tool {schema.name} signature mismatch") cls._tools[schema.name] = { 'func': func, 'schema': schema.dict(), 'sandbox_config': { 'cpu_limit': 0.2, 'mem_limit': '300m', 'timeout': 15 } } return func return decorator @classmethod def call_tool(cls, name: str, params: Dict[str, Any]) -> Dict[str, Any]: if name not in cls._tools: return {"error": f"Tool {name} not found"} tool_def = cls._tools[name] # 参数校验(用Pydantic) try: # 动态构建参数模型 param_model = create_param_model(tool_def['schema']['parameters']) validated_params = param_model(**params).dict() except ValidationError as e: return {"error": f"Param validation failed: {e}"} # 启动沙箱容器(简化版,实际用docker-py) result = run_in_sandbox( tool_def['func'], validated_params, tool_def['sandbox_config'] ) return result # 使用示例 @ToolRegistry.register(ToolSchema( name="fund_search", description="根据风险等级和预期收益筛选基金", parameters={ "type": "object", "properties": { "risk_level": {"type": "string", "enum": ["low", "medium", "high"]}, "min_return": {"type": "number", "minimum": 0} }, "required": ["risk_level"] }, returns={"type": "array", "items": {"type": "object"}} )) def fund_search(risk_level: str, min_return: float = 0.0) -> List[Dict]: # 真实业务逻辑 return [{"code": "000001", "name": "华夏成长", "fee_rate": 0.012}]关键细节:
create_param_model函数会根据JSON Schema动态生成Pydantic模型,确保运行时类型安全。而run_in_sandbox函数在实际生产中会调用Docker API启动隔离容器,这里为简洁省略。重点在于——所有工具调用都经过统一入口,框架层可在此处注入熔断、限流、审计日志。
3.3 流程引擎:用状态机+事件总线实现确定性编排
我们摒弃YAML配置,用纯Python定义状态机,确保IDE能跳转、调试器能断点。核心是StateMachine类,它接收状态定义和事件处理器。
# workflow_engine.py from enum import Enum from typing import Dict, Callable, Any, List import json class Event: def __init__(self, name: str, data: Dict[str, Any]): self.name = name self.data = data class StateMachine: def __init__(self, initial_state: str): self.current_state = initial_state self.transitions: Dict[str, Dict[str, str]] = {} # state -> event -> next_state self.guards: Dict[str, Callable] = {} # event -> guard_func self.handlers: Dict[str, Callable] = {} # state -> handler_func def add_transition(self, from_state: str, event: str, to_state: str, guard: Callable = None): if from_state not in self.transitions: self.transitions[from_state] = {} self.transitions[from_state][event] = to_state if guard: self.guards[event] = guard def add_handler(self, state: str, handler: Callable): self.handlers[state] = handler def handle_event(self, event: Event) -> Dict[str, Any]: # 1. 检查当前状态是否允许该事件 if self.current_state not in self.transitions: return {"error": f"No transitions defined for state {self.current_state}"} if event.name not in self.transitions[self.current_state]: return {"error": f"Event {event.name} not allowed in state {self.current_state}"} # 2. 执行守卫函数 if event.name in self.guards and not self.guards[event.name](event.data): return {"error": f"Guard failed for event {event.name}"} # 3. 执行当前状态处理器 if self.current_state in self.handlers: handler_result = self.handlers[self.current_state](event.data) if "error" in handler_result: return handler_result # 4. 状态迁移 next_state = self.transitions[self.current_state][event.name] self.current_state = next_state # 5. 执行新状态处理器 if next_state in self.handlers: return self.handlers[next_state](event.data) return {"state": next_state, "data": event.data} # 定义基金推荐工作流 fund_workflow = StateMachine("INIT") def init_handler(data: Dict[str, Any]) -> Dict[str, Any]: return {"message": "Start risk assessment"} def risk_assess_handler(data: Dict[str, Any]) -> Dict[str, Any]: # 调用风控模型 score = calculate_risk_score(data.get("answers", [])) return {"risk_score": score} def product_filter_guard(data: Dict[str, Any]) -> bool: # 守卫:风险分必须在30-90之间 return 30 <= data.get("risk_score", 0) <= 90 fund_workflow.add_handler("INIT", init_handler) fund_workflow.add_handler("RISK_ASSESS", risk_assess_handler) fund_workflow.add_transition("INIT", "start_assessment", "RISK_ASSESS") fund_workflow.add_transition("RISK_ASSESS", "risk_assessed", "PRODUCT_FILTER", product_filter_guard)经验之谈:状态机定义必须和业务文档强绑定。我们要求每个
add_transition调用旁必须加注释,说明该迁移对应的业务场景(如“用户完成风险问卷后触发”)。这样当业务方提出“增加‘跳过测评’分支”时,开发能精准定位到哪行代码修改,而不是全局搜索状态名。
3.4 可观测性埋点:用结构化日志+Trace ID实现全链路追踪
可观测性不是事后补救,而是从第一行代码就植入。我们采用“日志即指标”策略,所有关键操作都输出JSON日志,并携带唯一Trace ID。
# observability.py import logging import time import uuid from contextvars import ContextVar from typing import Dict, Any # 创建专用logger tracer_logger = logging.getLogger("agent.tracer") tracer_logger.setLevel(logging.INFO) handler = logging.StreamHandler() handler.setFormatter(logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s')) tracer_logger.addHandler(handler) # 上下文变量存储Trace ID trace_id_var = ContextVar("trace_id", default="") class Tracer: @staticmethod def start_trace(operation: str, **kwargs) -> str: trace_id = str(uuid.uuid4()) trace_id_var.set(trace_id) tracer_logger.info(json.dumps({ "event": "trace_start", "trace_id": trace_id, "operation": operation, "start_time": time.time(), "params": kwargs })) return trace_id @staticmethod def log_event(event: str, **kwargs): tracer_logger.info(json.dumps({ "event": event, "trace_id": trace_id_var.get(), "timestamp": time.time(), "data": kwargs })) @staticmethod def end_trace(success: bool, **kwargs): tracer_logger.info(json.dumps({ "event": "trace_end", "trace_id": trace_id_var.get(), "success": success, "end_time": time.time(), "duration_ms": (time.time() - kwargs.get("start_time", time.time())) * 1000, "result": kwargs.get("result") })) # 在Agent主流程中使用 def run_agent(user_id: str, input_text: str): trace_id = Tracer.start_trace("agent_execution", user_id=user_id, input=input_text) try: # 步骤1:状态加载 Tracer.log_event("state_load_start", user_id=user_id) user_state = StateManager().get_user_state(user_id) Tracer.log_event("state_load_end", state_keys=list(user_state.keys())) # 步骤2:LLM调用 Tracer.log_event("llm_call_start", prompt_len=len(input_text)) llm_result = call_llm(input_text, user_state) Tracer.log_event("llm_call_end", token_used=llm_result["tokens"]) # 步骤3:工具调用 if llm_result.get("tool_call"): Tracer.log_event("tool_call_start", tool_name=llm_result["tool_call"]["name"]) tool_result = ToolRegistry.call_tool( llm_result["tool_call"]["name"], llm_result["tool_call"]["params"] ) Tracer.log_event("tool_call_end", success="error" not in tool_result) Tracer.end_trace(success=True, result="completed") return llm_result except Exception as e: Tracer.end_trace(success=False, error=str(e)) raise生产技巧:JSON日志直接接入ELK栈,我们用Logstash的
json过滤器解析,将trace_id、event、duration_ms等字段提取为Kibana可分析的字段。这样运营同学就能自助查询:“过去24小时trace_id以abc开头的所有失败请求”,极大降低协同成本。
4. 避坑指南:Agent框架开发中踩过的12个真实深坑
纸上得来终觉浅,绝知此事要躬行。以下是我和团队在12个Agent项目中踩过的坑,每个都附带血泪教训和可落地的解决方案。这些坑不会出现在任何官方文档里,却是决定项目成败的关键。
4.1 坑位1:Prompt版本混乱导致线上效果雪崩
现象:某电商客服Agent上线后,用户投诉“回答越来越不准”,但模型版本、工具代码均未变更。
根因排查:通过Trace日志发现,同一用户会话中,前3轮调用的是v1.2 Prompt(强调“优先推荐促销商品”),第4轮突然切到v1.5(强调“严格按库存推荐”),原因是运维同学手动更新了Prompt文件,未做灰度发布。
解决方案:
- Prompt必须版本化管理,每个Prompt文件名含版本号(如
prompt_customer_service_v1.5.json); - 框架层强制要求LLM调用时指定
prompt_version参数,且该参数参与缓存key计算; - 上线新Prompt前,必须跑A/B测试:5%流量走新Prompt,对比“问题解决率”、“转人工率”等核心指标,达标后才全量。
教训:Prompt不是配置文件,而是核心业务逻辑。把它当成代码一样走CI/CD流程。
4.2 坑位2:工具调用超时未熔断,拖垮整个服务
现象:基金查询工具因第三方API故障,平均响应时间从200ms飙升至15s,导致Agent服务P99延迟突破30s,大量用户流失。
根因:工具调用未设置超时,且无熔断机制,请求在队列中堆积。
解决方案:
- 所有工具调用必须配置
timeout(单位秒)和max_retries(建议≤2); - 引入Hystrix式熔断器:当错误率>50%持续30秒,自动开启熔断,后续请求直接返回fallback结果(如“系统繁忙,请稍后再试”);
- 熔断状态存储在Redis,各实例共享,避免单点误判。
数据:加入熔断后,服务可用性从92.3%提升至99.99%,这是SLO的生死线。
4.3 坑位3:状态未隔离,用户A的数据污染用户B的会话
现象:用户A修改了风险偏好,用户B随后收到的推荐产品列表竟包含A偏好的类型。
根因:状态管理模块用了全局变量存储会话,未按user_id隔离。
解决方案:
- 状态对象必须绑定
user_id,禁止任何全局状态; - 在框架入口处强制校验:
if not request.user_id: raise AuthError("Missing user_id"); - 单元测试必须覆盖“并发请求不同user_id”的场景,用
concurrent.futures.ThreadPoolExecutor模拟。
警惕:这是最高发的安全漏洞,比SQL注入更隐蔽,因为不报错,只“错乱”。
4.4 坑位4:LLM输出JSON格式不稳定,导致工具调用失败
现象:基金查询工具调用失败率高达40%,日志显示LLM返回的是{"tool": "fund_search", "params": "{...}"},params字段是字符串而非对象。
根因:LLM对JSON Schema的理解不稳定,尤其在长上下文时易退化。
解决方案:
- 输出强制校验:用正则预检
params是否为合法JSON,否则触发重试; - 更优方案:改用XML格式(
<tool><name>fund_search</name><params><risk_level>medium</risk_level></params></tool>),LLM对XML标签的遵循率比JSON高27%; - 终极方案:在Prompt中明确指令“只输出XML,不要任何解释文字”,并在框架层用
xml.etree.ElementTree解析。
实测:XML方案将工具调用失败率从40%降至1.2%,且无需微调模型。
4.5 坑位5:未监控Token消耗,预算超支且无感知
现象:月度AI服务账单暴涨300%,财务部门紧急叫停项目。
根因:未对每次LLM调用的input/output token数做埋点,无法定位高消耗场景。
解决方案:
- 所有LLM SDK调用必须包装,自动记录
usage.total_tokens; - 按
user_id + task_type聚合,设置阈值告警(如单用户日token超50万触发邮件); - 对高消耗场景强制优化:如将长文档摘要改为分块处理,每块加
<chunk_index>标识,避免重复计算。
成果:通过监控,我们识别出“用户上传PDF合同全文”场景占总token 68%,针对性优化后节省42%成本。
4.6 坑位6:缺乏人工接管机制,用户问题无限循环
现象:用户多次询问“如何赎回基金”,Agent始终推荐购买产品,用户怒而投诉。
根因:未定义“人工接管”触发条件,Agent陷入无效循环。
解决方案:
- 定义三类接管信号:1)用户发送“转人工”;2)同一问题重复3次;3)Agent连续2次回复含“抱歉”“不确定”;
- 接管时自动打包完整Trace:包括所有状态快照、工具调用日志、LLM输入输出;
- 接管请求带
priority字段,根据用户VIP等级、问题紧急度(如“账户异常”标P0)路由。
用户体验:接管平均响应时间从15分钟降至92秒,投诉率下降76%。
4.7 坑位7:未做效果归因,无法证明Agent价值
现象:项目结项汇报时,老板问“Agent带来了多少业务增长?”,团队只能回答“用户说挺好”。
根因:未设计可量化的业务指标,所有数据停留在技术层。
解决方案:
- 定义北极星指标:如“智能投顾采纳率”(用户点击推荐产品的比例);
- 建立对照组:随机5%用户走传统网页流程,其余走Agent流程,AB测试;
- 归因分析:用Shapley值分解各模块贡献(如状态管理提升采纳率12%,工具沙箱提升8%)。
结果:我们用数据证明Agent使基金销售转化率提升22.3%,直接推动项目二期投入。
4.8 坑位8:本地开发环境与生产环境不一致
现象:开发时Agent响应飞快,上线后延迟飙升,排查发现是本地用OpenAI,生产用私有化部署的DeepSeek。
根因:环境配置硬编码,未抽象为配置中心。
解决方案:
- 抽象
LLMProvider接口,实现OpenAIProvider、DeepSeekProvider等; - 配置通过环境变量注入:
LLM_PROVIDER=deepseek LLM_MODEL=deepseek-chat-671b; - 本地启动脚本自动加载
.env.local,生产用K8s ConfigMap。
效率:新模型接入从3天缩短至2小时,且零代码修改。
4.9 坑位9:未处理LLM的“幻觉”,输出虚假信息
现象:Agent向用户推荐了一只不存在的基金“华夏稳盈增强债券A”,代码中并无此产品。
根因:LLM在知识盲区时自行编造,框架层未做事实核查。
解决方案:
- 所有涉及实体(基金、股票、法规)的输出,必须通过工具调用验证;
- 增加“事实核查”中间件:当LLM输出含
基金代码、公司名称等关键词,自动触发verify_entity工具; - 核查失败时,返回“暂未查到该产品信息”,绝不猜测。
安全底线:宁可答错,不可造假。这是金融场景的铁律。
4.10 坑位10:日志未脱敏,泄露用户隐私
现象:运维同学在排查问题时,将含用户身份证号的日志截图发到微信群,触发安全审计。
根因:日志打印未过滤敏感字段。
解决方案:
- 定义敏感字段白名单:
["id_card", "phone", "bank_account"]; - 日志拦截器自动替换:
"id_card": "110101********1234"; - 敏感操作(如资金变动)日志必须加密存储,密钥由KMS托管。
合规:这是GDPR、《个人信息保护法》的强制要求,非可选项。
4.11 坑位11:未做压力测试,高并发下状态错乱
现象:双十一大促期间,Agent服务在峰值QPS 5000时,部分用户状态丢失,推荐列表为空。
根因:SQLite连接池未配置,高并发下连接耗尽,状态写入失败静默。
解决方案:
- SQLite连接池大小=CPU核心数×2;
- 压测必须覆盖“混合读写”场景:70%读(get_state)、20%写(update_state)、10%工具调用;
- 状态写入失败必须抛异常,不可静默忽略。
数据:压测后,我们发现连接池从默认5提升至32,QPS承载能力从1800提升至5200。
4.12 坑位12:未设计降级方案,依赖服务宕机即全线崩溃
现象:风控模型服务宕机,Agent无法进行风险评估,整个服务不可用。
根因:未考虑依赖服务不可用的兜底策略。
解决方案:
- 依赖服务必须提供降级接口:如风控模型宕机时,返回预设的“中等风险”默认值;
- 降级策略可配置:通过Consul动态开关,运维可一键启用/禁用;
- 降级时自动告警,并记录降级次数,触发容量规划。
稳定性:加入降级后,服务SLA从99.5%提升至99.95%,这是生产环境的生命线。
5. 框架演进路线:从单体到平台的三年实践
一个成熟的Agent框架不会一蹴而就,它必然经历从“能用”到“好用”再到“平台化”的演进。我们团队花了三年时间,走过一条典型的螺旋上升路径,每一步都踩过坑,也收获了真知。
5.1 阶段一:单体框架(0-6个月)——解决“能不能跑”
这是最原始的阶段,目标只有一个:让Agent在测试环境跑通。我们用Flask搭了一个单文件服务,所有代码在一个app.py里,状态存在内存字典,工具调用直连API。优点是上手快,缺点是脆弱不堪——改一行代码就要全量重启,日志散落在终端,出了问题全靠print()。这个阶段的核心产出是验证了业务可行性:证明用LLM+工具组合确实能解决用户问题,值得投入。
关键决策:坚持“最小可行框架”原则。我们拒绝在第一版引入Redis、Kafka等重型组件,哪怕知道未来需要。因为过早复杂化会杀死探索欲。记住:第一个月