AI Agent 开发的最佳实践清单:20 条经验避免新人踩坑
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"。
更多推荐


所有评论(0)