AI Agent 的工程化实践全景图:从 prompt 工程到系统架构的完整知识体系

一、从"调 API"到"造 Agent"的认知跃迁

今年 4 月,我以为我已经懂了 AI Agent。毕竟我能用 langchain 搭一个简单的聊天机器人,能调用工具,能维护对话历史——这不就是 Agent 吗?

5 月份,我把这个"Agent"集成到 dayuan 里,作为"智能文件搜索"功能。用户说"帮我找到最近一周修改过的、和数据库连接相关的配置文件"。我的 Agent 的思考过程是:

  1. 理解用户意图 ✅
  2. 决定调用 find 工具 ✅
  3. 参数填错了:--name 写成了 --pattern
  4. 工具调用失败后不知道重试 ❌
  5. 返回给用户一句"没有找到相关文件" ❌

那天晚上我读了一篇论文——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 需要:

  1. 分析意图:用户到底想要什么?
  2. 分解任务:这个问题需要分几步?
  3. 选择工具:每步需要用什么工具?
  4. 执行并观察:工具返回了什么?
  5. 反思并调整:结果对吗?需要重试吗?
  6. 汇总输出:把中间步骤的结果整合成用户的答案

这个过程就是 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 资料来源索引,并在发布前将具体来源贴到对应断言之后。

Logo

Agent 垂直技术社区,欢迎活跃、内容共建。

更多推荐