AI Agent 开发的最佳实践清单:20 条经验避免新人踩坑


一、让我复盘一个烧了 200 美元的教训

两个月前,我写了一个自动分析 GitHub Issue 的 AI Agent。逻辑很简单:收到 issue → 调用 LLM 分析内容 → 打标签 → 分配给合适的开发者。测试环境跑了两天,效果很好。

然后我把它部署到线上,没加预算限制。

三天后收到 AWS 账单:$203.47。原因是 Agent 陷入了一个循环——对一个无法分类的 issue,它不断调用 LLM 尝试重新分析,每次调整一点点 prompt,永远找不到合适的标签。无限循环 + 每个循环 3000 tokens = 烧钱无底洞。

从那以后,我整理了一份 AI Agent 开发的最佳实践清单。不止是"加预算限制"(这个是事后诸葛亮),而是从架构设计阶段就应该考虑的问题。整理出 20 条,前 10 条讲架构,后 10 条讲运维。


二、Agent 核心决策流程

Agent 的核心决策流程通常始于用户输入或事件触发。系统首先进行意图分类:若意图明确,直接路由到专用工具;若意图模糊,则交由 LLM 进行推理拆解并生成子任务列表。随后逐个执行子任务,期间若需要更多信息,会追问用户或调用搜索,否则直接汇总结果。工具执行完毕后,同样进入结果汇总阶段。接下来进行结果质量校验,若通过则返回用户;若不通过,则检查重试次数是否小于 3 次。若是,调整策略重新执行;若否,则降级返回并标记人工介入。


三、前 10 条:架构设计阶段

1. 永远先做意图分类,不要直接丢给 LLM

不要把所有请求直接扔给 LLM 做自由推理。用一个轻量级分类器(甚至可以是规则匹配 + 小模型)先判断意图,然后路由到专用处理逻辑。节省 60% 以上的 token 消耗。

/// 意图分类器 —— 在调用大模型之前做路由

/// 用关键词匹配做一级分类,命中就直接走专用逻辑
#[derive(Debug, PartialEq)]
pub enum Intent {
/// 代码生成
CodeGeneration,
/// 问题解答
QuestionAnswering,
/// 文件操作
FileOperation,
/// 需要 LLM 推理的复杂意图
Complex(String),
}

pub fn classify_intent(user_input: &str) -> Intent {
let input_lower = user_input.to_lowercase();

// 关键词规则匹配 —— 不需要 LLM
if input_lower.contains("生成") || input_lower.contains("写一个") {
    Intent::CodeGeneration
} else if input_lower.contains("怎么") || input_lower.contains("为什么") {
    Intent::QuestionAnswering
} else if input_lower.contains("打开") || input_lower.contains("保存") {
    Intent::FileOperation
} else {
    // 无法匹配时标记为复杂意图,再交给 LLM
    Intent::Complex(user_input.to_string())
}

}


### 2. 给每个 LLM 调用加硬性预算上限

预算 = max_tokens(单次调用)+ max_iterations(总调用次数)+ timeout(总时间)。

```rust
use std::time::{Duration, Instant};
use tokio::time::timeout;

/// Agent 调用的预算配置
#[derive(Debug, Clone)]
pub struct AgentBudget {
    /// 单次 LLM 调用的最大 token 数
    pub max_tokens_per_call: u32,
    /// 总调用次数上限
    pub max_iterations: u32,
    /// 总执行时间上限
    pub timeout: Duration,
    /// 总 token 预算(所有调用加起来)
    pub total_token_budget: u32,
}

impl Default for AgentBudget {
    fn default() -> Self {
        Self {
            max_tokens_per_call: 4096,      // 单次最多 4096 tokens
            max_iterations: 10,              // 最多 10 轮
            timeout: Duration::from_secs(60), // 60 秒超时
            total_token_budget: 50000,       // 总 token 预算
        }
    }
}

/// 带预算控制的 Agent 执行器
pub struct BudgetedAgent {
    budget: AgentBudget,
    tokens_used: u32,
    iterations: u32,
}

impl BudgetedAgent {
    pub fn new(budget: AgentBudget) -> Self {
        Self {
            budget,
            tokens_used: 0,
            iterations: 0,
        }
    }

    /// 检查是否超过预算
    pub fn check_budget(&self) -> Result<(), AgentError> {
        if self.iterations >= self.budget.max_iterations {
            return Err(AgentError::BudgetExceeded(
                "超过最大迭代次数".to_string()
            ));
        }
        if self.tokens_used >= self.budget.total_token_budget {
            return Err(AgentError::BudgetExceeded(
                "超过总 token 预算".to_string()
            ));
        }
        Ok(())
    }
}

#[derive(Debug)]
pub enum AgentError {
    BudgetExceeded(String),
    Timeout(String),
    ToolError(String),
}

3. 工具调用做成幂等的

Agent 调用的每个工具函数应该可以安全地重复执行。做不到的(比如发送邮件),加去重标记。

use std::collections::HashSet;

/// 幂等工具调用包装器
/// 记录已执行的操作,防止重复执行
pub struct IdempotentTool {
    /// 已执行的操作 ID 集合
    executed: HashSet<String>,
}

impl IdempotentTool {
    pub fn new() -> Self {
        Self { executed: HashSet::new() }
    }

    /// 执行操作前检查是否已经执行过
    pub fn execute<F, T>(
        &mut self,
        operation_id: &str,
        f: F,
    ) -> Result<T, String>
    where
        F: FnOnce() -> Result<T, String>,
    {
        // 如果已经执行过,直接跳过
        if self.executed.contains(operation_id) {
            return Err(format!(
                "操作 {} 已经执行过,跳过重复调用",
                operation_id
            ));
        }

        // 标记为已执行
        self.executed.insert(operation_id.to_string());

        // 执行操作
        f()
    }
}

4. 给 LLM 输出加结构化校验层

LLM 的输出不可靠。永远在传给下一个环节之前做校验。

use serde::{Deserialize, Serialize};

/// Agent 行动 —— LLM 必须输出这个结构
#[derive(Debug, Deserialize, Serialize)]
pub struct AgentAction {
    /// 行动类型
    pub action: String,
    /// 参数
    pub params: serde_json::Value,
    /// 该行动的信心分数(0-1)
    pub confidence: f32,
}

/// 校验 LLM 输出的 Agent 行动
pub fn validate_action(output: &str) -> Result<AgentAction, String> {
    // 尝试解析 JSON
    let action: AgentAction = serde_json::from_str(output)
        .map_err(|e| format!("输出格式无效: {}", e))?;

    // 校验必填字段
    if action.action.is_empty() {
        return Err("action 字段不能为空".to_string());
    }

    // 校验信心分数范围
    if !(0.0..=1.0).contains(&action.confidence) {
        return Err(format!(
            "confidence 必须在 0-1 之间,当前值: {}",
            action.confidence
        ));
    }

    // 低信心标记 —— 可以触发人工审核
    if action.confidence < 0.5 {
        return Err(format!(
            "信心分数过低 ({:.2}),建议人工介入",
            action.confidence
        ));
    }

    Ok(action)
}

5. 环检测:最多调用 N 次

这是避免"烧钱循环"的最关键防线。用一个简单的计数器或者更高级的状态哈希。

6. 上下文窗口管理:滑动窗口 + 摘要

不要把整个对话历史喂给 LLM。超过窗口后,用摘要或滑动窗口裁剪。

7. 错误降级策略:Plan A → Plan B → 人工

每层都定义降级路径。LLM 解析失败 → 用规则匹配兜底。工具执行失败 → 返回友好错误信息。全链路失败 → 标记人工介入。

8. 工具函数的参数要做白名单校验

不要让 LLM 直接操作文件系统或数据库。所有工具调用先过校验层。

/// 文件操作工具 —— 只允许操作白名单中的路径
pub struct SafeFileTool {
    /// 允许访问的路径前缀白名单
    allowed_paths: Vec<std::path::PathBuf>,
}

impl SafeFileTool {
    /// 校验路径是否在白名单内
    fn validate_path(&self, path: &str) -> Result<std::path::PathBuf, String> {
        let canonical = std::path::PathBuf::from(path)
            .canonicalize()
            .map_err(|e| format!("路径无效: {}", e))?;

        // 检查是否在白名单目录下
        let allowed = self.allowed_paths.iter().any(|allowed_path| {
            canonical.starts_with(allowed_path)
        });

        if !allowed {
            return Err(format!(
                "安全限制:不允许访问路径 {}",
                canonical.display()
            ));
        }

        Ok(canonical)
    }
}

9. 流式输出:用户应该实时看到进展

不要让用户等 30 秒看到一个最终结果。Agent 的每个步骤都通过 SSE 或 WebSocket 实时推送。

10. 可观测性:每一步都要有日志和 trace

Agent 的调试难度是普通应用的 10 倍。因为 bug 不在代码里,在 LLM 的输出里。每一步调用都要记录:输入 prompt、输出结果、token 消耗、耗时。


四、后 10 条:运维与迭代

11. prompt 版本化管理

把 prompt 放在 Git 里管理,和代码一样做版本控制和 code review。

12. A/B 测试 prompt 效果

对同一批输入用两个不同 prompt 跑,对比成功率和 token 消耗。

13. 用更便宜的小模型做第一步推理

GPT-4o-mini 或本地模型做意图分类和简单任务,只在必要时调用大模型。

14. 缓存相似查询的结果

用 embedding 做语义相似度匹配,命中缓存直接返回。

15. 速率限制和排队

对接多个用户的 Agent 必须有速率限制,避免单个用户耗尽所有配额。

16. 敏感数据脱敏

传给 LLM 之前过滤密钥、密码、身份证号等敏感信息。

17. 用户反馈闭环

每个 Agent 输出后面跟一个"满意/不满意"按钮,收集数据用于评估。

18. 监控 token 消耗趋势

不是总消耗,是每天的消耗趋势。突然上涨说明可能出了问题。

19. 定期压测

Agent 的延迟和成本在真实负载下可能非线性增长。月活增长时提前做压测。

20. 永远保留"人工接管"入口

无论 Agent 多智能,用户必须能一键切到人工模式。


五、总结

AI Agent 开发和传统软件开发最大的区别是:你的代码是确定性的,但 LLM 的输出不是。这意味着常规的单元测试、集成测试不够用。Agent 的质量保证是一整套新方法论:prompt 评估、结果校验、预算控制、降级策略、人工兜底。

另外,Agent 和传统软件开发一样,越简单越可靠。别一上来就设计一个"万能 Agent"——它会调用任何工具、处理任何任务。从单一功能开始(比如只处理 GitHub Issue 分类),跑稳了再扩展。

Agent 的 80% 价值来自于 20% 的功能。如果你能把那 20% 做到极致的可靠,你就有资格说"我做出了一个好用的 Agent"。

Logo

中国智能体开发者社区,聚焦智能体与大模型开发,提供前沿资讯、实用工具链、开源项目及行业案例。通过技术沙龙、开发者大赛等活动,促进经验交流与协作,助力开发者快速构建创新智能应用。

更多推荐