AI Agent 的工程化实践全景图:从 prompt 工程到系统架构的完整知识体系
一、从"调 API"到"造 Agent"的认知跃迁
今年 4 月,我以为我已经懂了 AI Agent。毕竟我能用 langchain 搭一个简单的聊天机器人,能调用工具,能维护对话历史——这不就是 Agent 吗?
5 月份,我把这个"Agent"集成到 dayuan 里,作为"智能文件搜索"功能。用户说"帮我找到最近一周修改过的、和数据库连接相关的配置文件"。我的 Agent 的思考过程是:
- 理解用户意图 ✅
- 决定调用
find工具 ✅ - 参数填错了:
--name写成了--pattern❌ - 工具调用失败后不知道重试 ❌
- 返回给用户一句"没有找到相关文件" ❌
那天晚上我读了一篇论文——Stanford 的《Generative Agents》,里面有一句话点醒了我:"Agent 与简单 LLM 调用的核心区别在于,Agent 有内部状态和自主决策能力**。"**
我的理解一直停留在"把 LLM 的输出当命令来执行"的层面,而真正的 Agent 需要的是:记忆、规划、反思、纠错——这些不只是代码逻辑,更是系统架构。
这篇文章是我从 4 月到 7 月,从调 API 到设计和实现 dayuan Agent 系统的完整知识地图。
二、AI Agent 的五层架构
三、逐层拆解:dayuan 的 Agent 实现
第 1 层:Prompt 工程 —— 不是"写一段话",是"设计行为规范"
很多人理解的 prompt 工程是"调几个形容词,让 AI 回答得更好"。但 Agent 的 System Prompt 是它的"宪法"——定义了 Agent 是谁、能做什么、不能做什么、决策框架是怎样的。
/// dayuan 的 Agent System Prompt 结构 pub struct AgentPrompt { /// 角色定义:Agent 的身份和职责边界 role: String, /// 能力边界:Agent 能做什么,更重要的是——不能做什么 capabilities: Vec<Capability>, /// 工具使用协议:如何使用工具、如何解释工具输出 tool_use_protocol: String, /// 输出格式要求:期望 Agent 以什么格式输出 output_format: OutputFormat, /// 行为约束:安全规则和伦理限制 constraints: Vec<Constraint>, } impl AgentPrompt { /// 生成最终的 System Prompt /// 关键设计原则: /// ① 角色定义在前,建立稳定的行为基线 /// ② 工具描述用 JSON Schema,让 LLM 精确理解参数 /// ③ 行为约束用 MUST / MUST NOT,不模糊 pub fn build_system_prompt(&self) -> String { format!( "你是一个 {role}。\n\ \n\ 你的职责:\n\ {capabilities}\n\ \n\ 工具使用规则:\n\ {tool_protocol}\n\ \n\ 行为约束(违反将导致输出被拒绝):\n\ {constraints}\n\ \n\ 输出格式:{output_format}", role = self.role, capabilities = self.format_capabilities(), tool_protocol = self.tool_use_protocol, constraints = self.format_constraints(), output_format = self.output_format.to_instruction(), ) } }第 2 层:工具集成 —— MCP 协议的统一接口
工具的定义不能只是"一段自然语言描述"。LLM 需要精确知道工具名、参数类型、参数约束。我选择了 MCP(Model Context Protocol)作为 dayuan 的工具集成标准。
/// MCP 工具定义:用 JSON Schema 精确描述工具 #[derive(Debug, Serialize, Deserialize)] pub struct ToolDefinition { /// 工具唯一标识名 pub name: String, /// 人类可读的描述 pub description: String, /// 参数 JSON Schema pub input_schema: serde_json::Value, } /// 文件搜索工具的 MCP 定义 pub fn file_search_tool() -> ToolDefinition { ToolDefinition { name: "search_files".to_string(), description: "在项目目录中搜索匹配模式的文件,返回文件路径列表".to_string(), input_schema: serde_json::json!({ "type": "object", "properties": { "pattern": { "type": "string", "description": "glob 搜索模式,如 '**/*.rs' 或 'src/**/*.toml'" }, "content_regex": { "type": "string", "description": "可选的文件内容正则匹配,如 'database.*connect'" }, "max_results": { "type": "integer", "default": 20, "description": "返回的最大文件数" } }, "required": ["pattern"] // pattern 是必填参数! }), } } /// 工具调用执行器:解析 LLM 的工具调用请求 → 执行 → 返回结果 pub async fn execute_tool_call( tool_name: &str, arguments: serde_json::Value, project_root: &Path, ) -> Result<ToolResult, ToolError> { match tool_name { "search_files" => { // ① 解析参数 let pattern = arguments["pattern"] .as_str() .ok_or(ToolError::MissingArg("pattern"))?; // ② 执行搜索 let files = glob_search(project_root, pattern)?; // ③ 可选:内容过滤 let files = if let Some(regex) = arguments.get("content_regex") { filter_by_content(files, regex.as_str().unwrap())? } else { files }; // ④ 限制结果数量,避免上下文过载 let max = arguments["max_results"].as_u64().unwrap_or(20) as usize; let files: Vec<_> = files.into_iter().take(max).collect(); Ok(ToolResult { success: true, data: serde_json::to_value(&files)?, summary: format!("找到 {} 个匹配文件", files.len()), }) } _ => Err(ToolError::UnknownTool(tool_name.to_string())), } }第 3 层:记忆系统 —— ReAct 循环中的状态管理
Agent 不是无状态的函数调用。它需要记住"我刚才问了什么"、"刚才那步操作的结果是什么"、"用户的历史偏好是什么"。
/// Agent 的工作记忆:ReAct 循环中的状态 pub struct AgentMemory { /// 对话历史:支持滑动窗口,避免 token 超限 messages: VecDeque<ChatMessage>, /// 当前任务上下文:本次任务的关键信息 task_context: TaskContext, /// 工具调用历史:上次调了什么、什么结果 tool_call_history: Vec<ToolCallRecord>, /// 最大对话轮数(防止无限循环) max_turns: usize, } impl AgentMemory { /// 添加一轮对话,超出上限自动裁剪最早的 pub fn push_message(&mut self, msg: ChatMessage) { self.messages.push_back(msg); // 滑动窗口:超出上限时从最早的消息开始裁剪 // 但保留 System Prompt(第一条消息) while self.messages.len() > self.max_turns { if self.messages.len() > 1 { self.messages.remove(1); // 跳过 index 0 的 System Prompt } } } /// 构建发给 LLM 的消息历史 pub fn build_context(&self, current_task: &str) -> Vec<ChatMessage> { let mut context: Vec<ChatMessage> = self.messages.iter().cloned().collect(); // 注入当前任务的额外上下文 context.push(ChatMessage::system(format!( "当前任务上下文:项目 {project},分支 {branch}", project = self.task_context.project_name, branch = self.task_context.current_branch, ))); context } } /// 工具调用记录(供 Agent 反思和纠错) #[derive(Debug, Clone)] pub struct ToolCallRecord { pub tool_name: String, pub arguments: serde_json::Value, pub result: ToolResult, pub timestamp: chrono::DateTime<chrono::Utc>, pub retry_count: u32, }第 4 层:推理与规划 —— 让 Agent 先想再做
这是 Agent 和"简单 LLM 调用"最大的分水岭。传统的"用户问 → LLM 答"是单步推理,而 Agent 需要:
- 分析意图:用户到底想要什么?
- 分解任务:这个问题需要分几步?
- 选择工具:每步需要用什么工具?
- 执行并观察:工具返回了什么?
- 反思并调整:结果对吗?需要重试吗?
- 汇总输出:把中间步骤的结果整合成用户的答案
这个过程就是ReAct (Reasoning + Acting)循环。
/// ReAct 循环:Agent 的核心决策逻辑 pub async fn react_loop( agent: &mut Agent, user_input: &str, memory: &mut AgentMemory, ) -> Result<String, AgentError> { // 添加用户消息到记忆 memory.push_message(ChatMessage::user(user_input)); for turn in 0..memory.max_turns { // ① 构建上下文(包括历史、任务信息、可用工具列表) let context = memory.build_context(user_input); let tools = agent.get_available_tools(); // ② 调用 LLM 获取下一个动作 let response = agent.llm.chat_with_tools(&context, &tools).await?; match response.action { // ③ 如果 LLM 认为任务已完,返回最终答案 AgentAction::FinalAnswer(answer) => { memory.push_message(ChatMessage::assistant(&answer)); return Ok(answer); } // ④ 如果 LLM 需要调用工具,执行工具并记录结果 AgentAction::ToolCall { name, arguments } => { let result = execute_tool_call(&name, arguments, &agent.project_root).await; // 记录工具调用 memory.tool_call_history.push(ToolCallRecord { tool_name: name.clone(), arguments: arguments.clone(), result: result.clone(), timestamp: chrono::Utc::now(), retry_count: 0, }); // 将工具执行结果作为新的消息注入记忆 memory.push_message(ChatMessage::tool_result(&name, &result)); } } } Err(AgentError::MaxTurnsExceeded(memory.max_turns)) }第 5 层:多 Agent 协作 —— dayuan 的三角色模式
dayuan 的设计中有三个专业化 Agent:
四、我在 Agent 工程化中踩过的最大的三个坑
坑一:工具描述写得太模糊。LLM 需要精确的 JSON Schema,而不是"用 find 命令搜索文件"。模糊描述会导致 LLM 编造参数、遗漏必填项。
坑二:没有设置最大步数。一次用户说"帮我重构整个项目",Agent 跑了一个小时还没停,产生了 200+ 次工具调用。现在 dayuan 默认最大 15 步,超出就问用户"要继续吗?"
坑三:工具输出太大。搜索工具返回了 500 个文件路径,把上下文窗口直接撑爆。现在每个工具返回都带truncated标志和结果数量限制。
五、总结
从调 API 到造 Agent,核心的认知跃迁是:
Agent 不是"更聪明的 LLM",而是"被 LLM 驱动的软件系统"。
这意味着你需要考虑的不仅是 prompt 怎么写,还有:状态管理、错误恢复、资源限制、安全边界——这些是系统架构的问题,不是 NLP 的问题。
对于同学,我的建议是:不要一上来就做多 Agent 系统。从 ReAct 循环开始——一个 LLM + 两个工具 + 对话记忆——把这个最简系统调稳了,再往上叠复杂度。
所有伟大的框架(LangChain、AutoGPT、CrewAI)底层都是同样的核心循环。理解这个循环,你就理解了 Agent 的本质。
资料说明
本文中的协议、版本、性能、成本和行业趋势应以可核验的一手资料为准。未标注统计口径的比例、时间表和预测仅作工程讨论,不应视为行业事实。可参考 0731 资料来源索引,并在发布前将具体来源贴到对应断言之后。