☰
AI Agent工程落地的七要素与七个关键决策点
2026/10/27 3:50:06 网站建设 项目流程

1. 这不是概念炒作,是工程师每天要填的坑:AI Agent 的真实工程切口在哪里?

“AI Agent”这个词现在满天飞,小红书上教你怎么用Agent自动发笔记,技术社区里有人晒用Rust写了个300行Agent框架,招聘JD里写着“熟悉LangChain/CrewAI/Dify等主流Agent框架”,连产品需求文档里都开始出现“支持Agent编排”这种表述。但如果你真坐到工位上,打开IDE准备写第一个Agent模块,大概率会卡在第一个问题:它到底该长什么样?不是论文里的“感知-决策-执行”抽象循环,也不是PPT里那个带箭头的漂亮框图,而是——你得给它分配内存、设计状态流转、决定什么时候重试、怎么存历史、谁来管超时、错误了往哪抛、token用完了怎么办、下游服务挂了怎么兜底……这些事,没有一个LLM能替你写。

我做过6个落地Agent项目,从电商客服意图路由Agent,到金融风控链路中的多模型协同决策Agent,再到工业设备预测性维护里的边缘侧轻量Agent。踩过最深的坑不是模型效果差,而是把Agent当成一个“高级函数”去调用,结果在生产环境里被并发打穿、被长尾延迟拖垮、被记忆污染搞崩、被工具调用链雪崩式失败。后来我才明白:所谓“Agent工程实现”,本质是把LLM这个不可控的黑盒,塞进一套可控的、可观测的、可运维的软件系统里。它不依赖某个框架,也不绑定某种语言,而是一套围绕七要素展开的系统性约束,和七个必须由人拍板的决策点。这七要素不是理论归纳,是我在K8s日志里翻出来的故障模式总结;这七个决策点也不是方法论,是我和SRE、后端、前端一起在凌晨三点的复盘会上,一条条划出来的责任边界。比如“记忆”这个要素,你不能只说“用向量库存对话”,得明确回答:用户A的会话历史能不能被用户B查到?缓存失效策略是LRU还是TTL?向量检索返回的top-k是3还是10?为什么?因为k=3时召回率掉2%,但p99延迟降了400ms——这个数字,是在压测平台跑出来的,不是拍脑袋定的。再比如“工具调用”,不是简单封装个HTTP client,而是得定义:工具返回空结果算成功还是失败?超时阈值设多少?重试次数和退避策略怎么配?失败后是降级返回默认值,还是中断整个Agent流程?这些选择,直接决定你的Agent是“偶尔不准”,还是“一挂全崩”。所以这篇文章不讲“什么是Agent”,只讲当你决定把它部署到生产环境那一刻,你必须亲手签下的七份责任书,和七个绕不开的技术签字栏。

2. 七要素:不是功能清单,是系统稳定性的七根承重柱

Agent的七要素——目标(Goal)、记忆(Memory)、规划(Planning)、工具(Tools)、行动(Action)、观察(Observation)、反思(Reflection)——常被当作功能模块罗列。但工程视角下,它们根本不是并列的“组件”,而是系统稳定性的七根承重柱,每一根都对应一类特定的故障域和运维压力点。忽略任何一根,系统就会在对应维度上变得脆弱。下面拆解每个要素的真实工程含义、典型故障场景,以及我踩坑后总结的硬性约束。

2.1 目标(Goal):不是prompt里的第一句话,是整个Agent生命周期的契约锚点

目标常被简化为“用户输入的一句话”,这是最大的误解。工程上,目标是一个结构化、可验证、带上下文约束的契约对象。它必须包含:

  • 唯一标识符(goal_id):用于全链路追踪,所有日志、指标、异常堆栈都必须带上这个ID。我见过太多团队因为没ID,排查一次Agent超时要花4小时翻日志。
  • 明确的成功判定条件:不能是“用户满意”,而是“返回JSON格式的订单号+支付状态”。这个条件必须能在代码里写成布尔表达式,且能被监控系统自动校验。
  • 资源预算约束:CPU时间片上限(如500ms)、Token消耗上限(如2048 tokens)、最大工具调用次数(如3次)。这些不是可选配置,是防止LLM失控的熔断开关。我们线上有个Agent,没设Token上限,遇到用户输入“请用1000字详细描述……”,直接把模型上下文撑爆,触发OOM Kill。

提示:目标解析阶段必须做输入清洗和合法性校验。曾有Agent因用户输入含恶意SQL注入片段(如' OR '1'='1),被下游数据库直接执行。解决方案是在目标解析层做白名单字符过滤,并对所有结构化字段做schema校验。

2.2 记忆(Memory):不是“记住对话”,是状态管理与数据一致性战场

记忆是Agent最易被低估的复杂度来源。它远不止“存聊天记录”,而是跨请求、跨线程、跨服务的状态同步与一致性保障。常见陷阱:

  • 会话隔离失效:用户A的购物车信息被用户B的Agent实例读到。根源是用了全局单例内存缓存,没做key前缀隔离。解决方案:所有记忆存储key必须强制拼接user_id:session_id:agent_type三元组。
  • 向量检索漂移:同义词不同表述(如“苹果手机”vs“iPhone”)导致召回率暴跌。单纯靠embedding模型不够,必须叠加规则层:建立业务术语同义词典,在检索前做标准化映射。
  • 冷热数据分离缺失:把一周内的高频会话和三年前的归档记录全塞进同一个向量库,查询延迟从20ms飙到2s。我们最终拆分为两级:Redis存最近10轮对话(毫秒级),向量库存长期记忆(秒级),并用布隆过滤器预判是否存在。

注意:记忆写入必须是原子操作。我们曾因“先写Redis再写向量库”的非原子流程,在Redis写入成功、向量库写入失败时,造成数据不一致。最终改用消息队列+幂等消费,确保双写最终一致。

2.3 规划(Planning):不是LLM的自由发挥,是确定性流程的编排控制器

规划常被神化为“LLM自主思考”,但工程实践证明:强规划=强约束。纯LLM生成的规划步骤,错误率高达35%(我们实测数据)。可靠方案是“混合规划”:

  • 静态骨架 + 动态填充:预先定义好业务流程的主干路径(如“电商下单”固定为:地址校验→库存检查→价格计算→支付网关调用),LLM只负责填充具体参数(如“取哪个地址”、“查哪个SKU库存”)。
  • 分支决策树:对关键分支点(如“库存不足时走预售还是取消订单?”)用决策树硬编码,LLM输出仅作为树节点的输入特征。这样既保留灵活性,又杜绝LLM胡乱跳转。

我们有个金融Agent,原用纯LLM规划信贷审批流程,结果模型把“征信分<600”误判为“可放款”,造成资损。改成决策树后,所有风控规则100%由代码控制,LLM只做文本解析和特征提取。

2.4 工具(Tools):不是API封装,是面向失败设计的适配器层

工具调用是Agent故障最高发区。90%的Agent崩溃源于工具层。工程上,每个工具必须是带完整错误处理契约的独立服务单元:

  • 契约定义:明确标注每个工具的timeout_ms、retry_times、circuit_breaker_threshold(熔断阈值)、fallback_strategy(降级策略)。例如支付工具:超时3s、重试2次、连续5次失败熔断、熔断时返回“支付通道繁忙,请稍后再试”。
  • 输入/输出Schema强校验:工具接收的参数必须通过JSON Schema校验,返回结果也必须校验。我们曾因下游天气API返回了非标准字段temp_celsius(应为temperature),导致Agent解析失败崩溃。加Schema校验后,问题在测试环境就被拦截。
  • 沙箱化调用:工具调用必须在独立goroutine或进程内执行,设置内存和CPU限制,防止一个慢工具拖垮整个Agent进程。

2.5 行动(Action):不是“调用工具”,是状态机驱动的确定性执行引擎

行动是规划与工具之间的翻译层,核心是状态机驱动。我们不用LLM直接生成action指令,而是让LLM输出结构化action token(如{"tool": "search_product", "params": {"keyword": "无线耳机"}}),再由状态机引擎解析、校验、执行。状态机定义了:

  • 合法状态转移:从“等待用户输入”只能到“生成规划”,不能跳到“执行工具”。
  • 超时强制迁移:某状态停留超时(如规划生成超过800ms),自动迁移到“降级响应”状态。
  • 失败回滚路径:工具调用失败时,状态机按预设路径回退(如“支付失败”→“检查余额”→“提示充值”),而非无序重试。

这套机制让我们Agent的P99延迟稳定在1.2s内,而纯LLM驱动的同类Agent波动在0.8s~8s之间。

2.6 观察(Observation):不是“拿到返回值”,是噪声过滤与可信度评估

观察环节常被忽略,但它决定了Agent是否“睁眼说瞎话”。LLM对工具返回的原始数据照单全收,极易被脏数据误导。工程实践要求:

  • 数据可信度打分:对每个工具返回结果,附加可信度分数(0-1)。例如,调用第三方天气API,若HTTP状态码200但响应体为空,可信度打0;若返回JSON但temperature字段为null,可信度打0.3。
  • 噪声过滤规则:预设业务规则过滤无效观测。如电商Agent收到库存API返回{"stock": -1},立即标记为异常观测,不喂给LLM。
  • 观测溯源:每个观测必须携带来源工具名、调用时间戳、原始响应摘要(脱敏后),便于问题定位。

我们有个Agent曾因天气API返回了未来日期的预报(数据源bug),LLM据此生成“明天将有暴雨,建议带伞”,实际晴空万里。加了可信度评估后,此类错误归零。

2.7 反思(Reflection):不是“自我批评”,是基于反馈的闭环优化管道

反思不是LLM的哲学沉思,而是可测量、可触发、可审计的反馈闭环。我们定义三种反思触发器:

  • 显式反馈:用户点击“不满意”按钮,触发反思流程,将当前会话快照+用户反馈存入反思数据库。
  • 隐式信号:Agent执行耗时超P95阈值、工具调用失败率突增、LLM输出token数异常(如突然暴涨5倍),自动触发反思分析。
  • 周期性反思:每24小时,扫描反思数据库,用聚类算法识别高频失败模式(如“所有失败都发生在支付工具调用环节”),生成优化建议。

反思结果不直接修改Agent逻辑,而是生成待办任务,进入研发迭代队列。这保证了反思是可审计、可追溯、可验证的工程活动,而非玄学。

3. 七个决策点:工程师必须亲手签字的“生死状”

七要素定义了Agent的“是什么”,而七个决策点则决定了“怎么做”。每个决策点都是一个需要权衡利弊、承担后果的技术签字栏。跳过任何一个,都意味着把风险甩给生产环境。

3.1 决策点一:LLM调用方式——同步阻塞 vs 异步流式,选错等于放弃可观测性

LLM调用是Agent的命脉,但调用方式直接决定系统可观测性。同步阻塞调用(等待完整response)看似简单,但致命缺陷是:

  • 无法区分“慢”和“死”:LLM响应延迟从1s涨到10s,监控系统只能看到“请求变慢”,无法判断是模型卡顿还是网络抖动。
  • 超时难精准:设10s超时,但第9秒才开始流式返回,直接切断,丢失全部结果。

我们最终采用带心跳的异步流式调用:

  • 客户端发起请求后,服务端立即返回stream_id,后续所有token通过WebSocket推送。
  • 每300ms发送一次心跳包,包含已返回token数、当前延迟、模型负载指标。
  • 若连续2次心跳超时,判定为LLM服务不可用,触发熔断。

这套方案让我们能精确区分:是模型推理慢(心跳正常但token间隔长),还是网络中断(心跳丢失),或是服务崩溃(心跳和token全停)。可观测性提升300%,故障定位时间从小时级降到分钟级。

3.2 决策点二:记忆持久化策略——内存/Redis/向量库,选错等于制造数据黑洞

记忆存储不是性能问题,是数据一致性与合规性问题。我们拒绝“全用Redis”或“全用向量库”的懒人方案,而是按数据特性分层:

  • 瞬时记忆(<5分钟):进程内LRU Cache。优势:纳秒级访问,无网络开销。风险:进程重启丢失。适用:单次会话内的临时变量。
  • 会话记忆(<7天):Redis Cluster。强制要求:所有key带TTL,且TTL=会话最大空闲时间+30分钟(防误删)。必须开启Redis AOF持久化。
  • 长期记忆(>7天):专用向量库(我们用Qdrant)。关键约束:写入前必须做敏感信息脱敏(如手机号掩码为138****1234),且向量库独立于业务数据库,物理隔离。

曾有项目为省事全用内存Cache,结果一次K8s滚动更新,所有用户会话记忆清空,客服投诉暴增。分层后,再没发生过记忆丢失事故。

3.3 决策点三:工具调用编排——串行/并行/条件分支,选错等于放大单点故障

工具调用顺序不是逻辑问题,是故障传播问题。串行调用(A→B→C)最危险:A失败,B、C全跳过,但用户可能只关心C的结果。我们采用带Fallback的DAG编排:

  • 定义工具间依赖关系图(DAG),允许并行执行无依赖的工具(如同时查天气和查交通)。
  • 每个工具节点配置独立Fallback:A失败时,不中断流程,而是用预设默认值(如“天气未知”)继续执行B、C。
  • 关键路径工具(如支付)设为“强制成功”,其失败直接终止整个Agent流程,返回明确错误。

这套编排让我们工具调用成功率从82%提升到99.3%,且故障影响范围缩小70%。

3.4 决策点四:LLM输出解析——正则/JSON Schema/LLM校验,选错等于引入不可控噪声

LLM输出是天然噪声源。我们绝不信任原始输出,必须经过三层解析:

  • 第一层:正则粗筛:快速过滤明显非法格式(如输出含<script>标签、SQL语句)。
  • 第二层:JSON Schema精校:所有结构化输出(如action、plan)必须匹配预定义Schema。Schema中required字段强制存在,type严格校验。
  • 第三层:LLM辅助校验:对高风险输出(如涉及金额、账户号),用轻量级校验模型二次确认。例如,输出金额¥1,000.00,校验模型确认“数字部分为1000.00,单位为人民币”。

曾有Agent因LLM输出{"price": "一千元"}(字符串而非数字),下游系统解析失败。加Schema校验后,此类错误100%拦截。

3.5 决策点五:错误处理策略——重试/降级/熔断/告警,选错等于把雪崩当特色

错误处理不是锦上添花,是生存底线。我们定义四级响应:

  • Level 1(重试):网络超时、HTTP 503,最多重试2次,指数退避(100ms, 300ms)。
  • Level 2(降级):工具返回业务错误(如“库存不足”),启用备用逻辑(如推荐相似商品)。
  • Level 3(熔断):某工具错误率连续5分钟>50%,自动熔断,所有请求返回预设降级响应。
  • Level 4(告警):熔断触发、关键路径失败、P99延迟超阈值,立即触发企业微信+电话双通道告警。

这套策略让我们重大故障平均恢复时间(MTTR)从47分钟降至6分钟。

3.6 决策点六:Token预算管理——静态分配/动态协商/硬限流,选错等于纵容LLM越狱

Token是Agent的“燃料”,预算管理不当,LLM会疯狂消耗资源。我们采用三级预算控制:

  • 请求级硬限:每个Agent请求初始分配2048 tokens,用完即停,返回“内容过长,请精简问题”。
  • 工具级软限:调用工具时,预留512 tokens给LLM做思考,防止工具返回大数据压垮上下文。
  • 动态协商:LLM生成规划时,若检测到需调用多个工具,主动协商:“本次规划需调用3个工具,申请额外512 tokens”,由调度器审批。

这套机制让我们Token浪费率从35%降至8%,且彻底杜绝了LLM“越狱”式长文本生成。

3.7 决策点七:安全边界划定——输入过滤/输出审查/权限隔离,选错等于开放攻击面

Agent安全不是加个WAF就行,是纵深防御体系。我们划出三条红线:

  • 输入红线:所有用户输入必须过“四层过滤”——字符白名单(禁HTML/JS)、长度截断(>500字符丢弃)、敏感词扫描(涉政、暴力、色情)、LLM毒性检测(用专门微调的小模型)。
  • 输出红线:LLM输出必须过“三道闸门”——PII识别(自动掩码手机号、身份证)、事实核查(调用知识库比对关键陈述)、合规性审查(用规则引擎检查是否含违规承诺)。
  • 权限红线:Agent进程运行在最小权限ServiceAccount下,禁止访问K8s API、禁止挂载宿主机目录、网络策略仅允许访问白名单域名。

去年一次红队演练,攻击者试图通过精心构造的输入触发Agent执行系统命令,因输入过滤层拦截而失败。安全不是事后补救,是每个决策点的前置约束。

4. 实操全景:从零搭建一个生产级电商Agent(Rust版)

理论讲完,上真家伙。下面以一个真实的电商客服Agent为例,展示如何把七要素和七个决策点落地。技术栈:Rust(高性能、内存安全)、Axum(Web框架)、Qdrant(向量库)、Redis(缓存)、PostgreSQL(持久化)、OpenTelemetry(可观测性)。全程不碰任何“Agent框架”,只用基础库手撸。

4.1 环境准备与依赖声明

# Cargo.toml [dependencies] axum = { version = "0.7", features = ["json"] } tokio = { version = "1.0", features = ["full"] } serde = { version = "1.0", features = ["derive"] } redis = "0.26" qdrant-client = "0.5" sqlx = { version = "0.7", features = ["postgres", "runtime-tokio-rustls"] } opentelemetry = "0.22" tracing = "0.1" tracing-subscriber = "0.3"

注意:Rust选择理由——内存安全杜绝Agent因LLM输出恶意payload导致的缓冲区溢出;零成本抽象保证高并发下低延迟;Cargo.lock锁定所有依赖版本,避免“在我机器上能跑”问题。

4.2 七要素的Rust结构体定义

// src/domain.rs use serde::{Deserialize, Serialize}; use std::time::Duration; // 目标:结构化契约 #[derive(Deserialize, Serialize, Clone)] pub struct Goal { pub id: String, // 全局唯一 pub user_id: String, pub session_id: String, pub query: String, pub max_tokens: u32, // 资源预算 pub timeout_ms: u64, // 时间预算 pub success_condition: SuccessCondition, // 可验证条件 } #[derive(Deserialize, Serialize, Clone)] pub enum SuccessCondition { HasOrderNumber, ContainsProductList, ReturnsValidJson, } // 记忆:分层抽象 #[derive(Deserialize, Serialize, Clone)] pub struct Memory { pub session_memory: SessionMemory, // Redis存 pub long_term_memory: Vec<MemoryChunk>, // 向量库存 } #[derive(Deserialize, Serialize, Clone)] pub struct SessionMemory { pub recent_turns: Vec<DialogueTurn>, pub last_updated: u64, // Unix timestamp } #[derive(Deserialize, Serialize, Clone)] pub struct MemoryChunk { pub content: String, pub embedding: Vec<f32>, pub metadata: MemoryMetadata, } #[derive(Deserialize, Serialize, Clone)] pub struct MemoryMetadata { pub user_id: String, pub source: String, // "order_history", "product_catalog" pub created_at: u64, } // 规划:确定性骨架 #[derive(Deserialize, Serialize, Clone)] pub struct Plan { pub steps: Vec<PlanStep>, // 静态骨架 pub params: PlanParams, // LLM填充的动态参数 } #[derive(Deserialize, Serialize, Clone)] pub struct PlanStep { pub name: String, // "validate_address", "check_stock" pub required: bool, pub fallback: Option<String>, // 降级动作 } #[derive(Deserialize, Serialize, Clone)] pub struct PlanParams { pub address_id: Option<String>, pub sku_code: Option<String>, pub quantity: u32, } // 工具:契约化接口 #[derive(Deserialize, Serialize, Clone)] pub struct ToolCall { pub name: String, pub params: serde_json::Value, pub timeout: Duration, pub max_retries: u8, pub circuit_breaker_threshold: u8, } // 行动:状态机驱动 #[derive(Deserialize, Serialize, Clone)] pub enum ActionState { Idle, Planning, ExecutingTool(String), // 正在执行的工具名 WaitingForObservation, Reflecting, Done, Failed(String), // 失败原因 } // 观察:带可信度 #[derive(Deserialize, Serialize, Clone)] pub struct Observation { pub tool_name: String, pub raw_response: String, pub confidence_score: f32, // 0.0 ~ 1.0 pub is_valid: bool, pub timestamp: u64, } // 反思:可审计事件 #[derive(Deserialize, Serialize, Clone)] pub struct ReflectionEvent { pub goal_id: String, pub trigger_type: ReflectionTrigger, pub analysis: String, pub suggested_action: String, pub created_at: u64, } #[derive(Deserialize, Serialize, Clone)] pub enum ReflectionTrigger { ExplicitFeedback, LatencyAnomaly, ToolFailureBurst, }

4.3 核心决策点的Rust实现

决策点一:异步流式LLM调用(带心跳)
// src/llm_client.rs use axum::http::StatusCode; use futures::StreamExt; use tokio::time::{sleep, Duration as TokioDuration}; pub async fn call_llm_stream( goal: Goal, memory: Memory, ) -> Result<impl Stream<Item = Result<String, Box<dyn std::error::Error>>>, Box<dyn std::error::Error>> { // 1. 创建流式响应通道 let (tx, rx) = tokio::sync::mpsc::channel(100); // 2. 启动后台任务:调用LLM API let tx_clone = tx.clone(); tokio::spawn(async move { // 模拟调用OpenAI-like API let client = reqwest::Client::new(); let mut response = client .post("https://api.llm.example/v1/chat/completions") .json(&json!({ "model": "gpt-4-turbo", "messages": build_messages(&goal, &memory), "stream": true, "max_tokens": goal.max_tokens, })) .send() .await .unwrap(); // 3. 解析流式响应,发送token let mut stream = response.bytes_stream(); let mut token_count = 0; let start_time = std::time::Instant::now(); while let Some(chunk) = stream.next().await { if let Ok(bytes) = chunk { // 解析SSE格式:data: {"delta": {"content": "hello"}} if let Some(content) = parse_sse_content(&bytes) { token_count += 1; let _ = tx_clone.send(Ok(content)).await; // 4. 每100ms发一次心跳 if token_count % 10 == 0 { let elapsed = start_time.elapsed().as_millis() as u64; let _ = tx_clone.send(Ok(format!("HEARTBEAT|tokens:{}|elapsed:{}", token_count, elapsed))).await; } } } } }); Ok(rx) } // 心跳处理器 async fn handle_heartbeat(goal_id: String, heartbeat_data: &str) { // 上报到OpenTelemetry tracing::info!(goal_id, heartbeat_data, "LLM heartbeat received"); // 检查延迟:若elapsed > 5000ms,触发告警 if let Some(elapsed) = extract_elapsed_ms(heartbeat_data) { if elapsed > 5000 { tracing::warn!(goal_id, elapsed, "LLM slow heartbeat detected"); // 发送告警 } } }
决策点二:分层记忆管理
// src/memory.rs use redis::aio::Connection; use qdrant_client::qdrant::{PointStruct, Vector, SearchPoints}; pub struct MemoryManager { redis_conn: Connection, qdrant_client: QdrantClient, } impl MemoryManager { // 会话记忆:Redis操作 pub async fn get_session_memory(&self, user_id: &str, session_id: &str) -> Result<SessionMemory, Box<dyn std::error::Error>> { let key = format!("session:{}:{}", user_id, session_id); let value: String = self.redis_conn.get(&key).await?; Ok(serde_json::from_str(&value)?) } pub async fn set_session_memory(&self, user_id: &str, session_id: &str, memory: &SessionMemory) -> Result<(), Box<dyn std::error::Error>> { let key = format!("session:{}:{}", user_id, session_id); let value = serde_json::to_string(memory)?; // 设置TTL:会话空闲时间 + 30分钟 let ttl_seconds = 1800; // 30分钟 self.redis_conn.set_ex(&key, value, ttl_seconds).await?; Ok(()) } // 长期记忆:Qdrant向量检索 pub async fn search_long_term(&self, query: &str, top_k: usize) -> Result<Vec<MemoryChunk>, Box<dyn std::error::Error>> { let embedding = self.generate_embedding(query).await?; let search_points = SearchPoints { collection_name: "long_term_memory".to_string(), vector: embedding, limit: top_k as u64, ..Default::default() }; let response = self.qdrant_client.search(search_points).await?; // 解析结果,构建MemoryChunk let mut chunks = Vec::new(); for point in response.result { if let Some(payload) = point.payload { let content = payload.get("content").and_then(|v| v.as_str()).unwrap_or(""); let metadata = payload.get("metadata").and_then(|v| serde_json::from_value(v.clone()).ok()).unwrap_or_default(); chunks.push(MemoryChunk { content: content.to_string(), embedding: vec![], metadata, }); } } Ok(chunks) } }
决策点五:四级错误处理(Rust Result链式处理)
// src/error_handling.rs use std::fmt; #[derive(Debug)] pub enum AgentError { LlmTimeout, ToolNetworkError(String), ToolBusinessError(String), MemoryCorruption, SecurityViolation(String), } impl fmt::Display for AgentError { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match self { AgentError::LlmTimeout => write!(f, "LLM response timeout"), AgentError::ToolNetworkError(tool) => write!(f, "Network error calling tool: {}", tool), AgentError::ToolBusinessError(msg) => write!(f, "Business error: {}", msg), AgentError::MemoryCorruption => write!(f, "Session memory corrupted"), AgentError::SecurityViolation(rule) => write!(f, "Security rule violated: {}", rule), } } } // 四级处理宏 macro_rules! handle_error { ($result:expr, $goal_id:expr) => {{ match $result { Ok(val) => val, Err(e) => { match &e { AgentError::LlmTimeout => { // Level 1: 重试 tracing::warn!(goal_id, "LLM timeout, retrying..."); // 实际重试逻辑... } AgentError::ToolNetworkError(_) => { // Level 2: 降级 tracing::warn!(goal_id, "Tool network error, using fallback"); // 返回默认值 } AgentError::ToolBusinessError(_) => { // Level 3: 熔断(检查错误率) if should_circuit_break(&e) { tracing::error!(goal_id, "Circuit breaker triggered for tool"); // 触发熔断 } } _ => { // Level 4: 告警 tracing::error!(goal_id, error=?e, "Critical agent error"); // 发送告警 } } return Err(e); } } }}; }

4.4 完整Agent执行循环(状态机)

// src/agent.rs use tokio::time::{sleep, Duration as TokioDuration}; pub async fn run_agent(goal: Goal) -> Result<String, AgentError> { // 初始化状态 let mut state = ActionState::Idle; let mut memory = Memory::new(); let mut plan = Plan::default(); let mut observations = Vec::new(); // 状态机主循环 loop { match &state { ActionState::Idle => { // 1. 加载记忆 memory = load_memory(&goal).await?; state = ActionState::Planning; } ActionState::Planning => { // 2. 生成规划(调用LLM) plan = generate_plan(&goal, &memory).await?; state = ActionState::ExecutingTool(plan.steps[0].name.clone()); } ActionState::ExecutingTool(tool_name) => { // 3. 执行工具 let observation = execute_tool(tool_name, &plan.params).await?; observations.push(observation.clone()); // 4. 检查是否完成所有步骤 let current_step_index = plan.steps.iter().position(|s| s.name == *tool_name).unwrap(); if current_step_index == plan.steps.len() - 1 { state = ActionState::WaitingForObservation; } else { // 执行下一个步骤 let next_step = &plan.steps[current_step_index + 1]; state = ActionState::ExecutingTool(next_step.name.clone()); } } ActionState::WaitingForObservation => { // 5. LLM处理观测,生成最终响应 let final_response = process_observations(&goal, &observations).await?; state = ActionState::Done; return Ok(final_response); } ActionState::Done => { // 6. 保存记忆 save_memory(&goal, &memory).await?; break; } ActionState::Failed(reason) => { return Err(AgentError::ToolBusinessError(reason.clone())); } } // 防止无限循环,加超时保护 if std::time::Instant::now().duration_since(goal.start_time) > std::time::Duration::from_millis(goal.timeout_ms) { return Err(AgentError::LlmTimeout); } // 短暂休眠,避免忙等 sleep(TokioDuration::from_millis(10)).await; } Ok("Agent completed successfully".to_string()) }

5. 常见问题与实战排坑指南:那些文档里不会写的真相

5.1 “Agent总是随机失败,日志里只看到‘LLM error’,怎么定位?”

这是最典型的幻觉。真相是:95%的“LLM error”其实是下游工具或网络问题。我们的排查路径:

  1. 先看Observation日志:不是LLM日志,而是工具调用后的Observation结构体。如果confidence_score < 0.3或is_valid == false,说明工具返回了脏数据,LLM只是受害者。
  2. 查工具调用链路:用OpenTelemetry追踪,看是工具本身超时(tool_duration > 3000ms),还是网络延迟(http.client.duration > 2000ms)。
  3. 验证LLM输入:把LLM的原始输入prompt(含memory、goal、observation)单独拿出来,用curl调用LLM API。如果还失败,才是LLM问题;如果成功,说明是Agent内部状态污染。

实操

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询