ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

AI Agent工程化落地:从七要素到七个关键决策

AI Agent工程化落地:从七要素到七个关键决策 聊到 AI Agent很多团队卡在工程实现上。我最近在帮几个团队做 Agent 落地发现大家手里都有一个能跑通 Demo 的脚本但真要把它变成“上线后不出乱子”的服务往往会死在上下文管理、工具调用、记忆污染这些细节上。做 Agent 工程我的经验是先抓住两件事一是把 Agent 拆成七要素去理解二是用七个决策点去约束实现。所谓七要素是指目标设定、环境感知、规划拆解、记忆管理、工具调用、行动执行、反馈反思所谓七个决策点是你在工程化过程中绕不开的模型选型、编排架构、记忆策略、工具协议、上下文管理、任务规划、可观测性。这篇文章会把每个要素落到代码里的位置把每个决策点背后的取舍讲清楚。适合正在做 AI 应用、被 Agent 稳定性折磨的开发者如果你是刚入门也可以把它当作 Agent 的学习路线照着搭一个最小闭环。1. 先拆清楚七要素分别对应工程里的哪个模块1.1 Agent 与普通接口调用的本质区别很多人理解的 AI Agent 是“多轮对话 调用工具”这在工程上还不够。普通接口调用是输入输出固定、错误可预期Agent 则是一个可以自主决策的控制循环。它接收一个目标自己决定下一步调用哪个工具、什么时候结束、如果失败如何修正。这个区别决定了你不能用写 CRUD 的思路来处理 Agent。生活化类比一下普通程序像自动售货机你投币选货它给你一瓶水Agent 更像一个帮你跑腿办事的人你说“去买菜准备晚餐”他需要自己列清单、看冰箱库存、决定去哪个菜市场、买回来发现缺盐还会再跑一趟。这个人之所以能完成复杂任务不是因为“嘴皮子厉害”而是因为他有目标、有记忆、有工具、有反馈。Agent 工程实现要做的事就是把这类能力拆成模块然后串成一个闭环。这个闭环的链条是目标设定 - 环境感知 - 规划拆解 - 记忆读写 - 工具调用 - 行动执行 - 反馈反思。任何一环缺失Agent 都只能算一个“会聊天的接口”。我在跟团队评审方案时会先让他们把这个链条画出来再谈技术选型否则后面一定会陷入“模型很聪明但系统很呆”的尴尬。1.2 七要素逐项拆解每个要素都有明确的工程落点按我的工程习惯七要素最终都会落到代码里。下面这表格是我在方案评审时常用的对照七要素工程落点常见的坑目标设定系统提示词、任务状态定义、终止条件目标太模糊模型不知道什么时候算完成环境感知输入解析、业务数据接入、用户/环境上下文只关注用户直接输入忽略外部状态规划拆解规划 Prompt、任务状态机、子任务队列让模型自由发挥没有流程兜底记忆管理短期对话 Buffer、工作记忆、长期向量库所有内容都塞进上下文几千字后效果断崖工具调用Function Schema、工具注册表、执行器Schema 描述不清模型反复传错参数行动执行工具结果返回、外部 API 调用、落库忽略执行异常模型拿错误结果继续推理反馈反思结果校验、错误重试、事后评测没有反馈闭环错了也不知道目标设定并不是写一句“你是一个助手”就完了。工程上要定义什么叫做“完成”。比如会议纪要助手完成可能是“输出一份结构化纪要和 3 条待办”而不是“聊到 10 轮后自动停止”。没有显式的终止条件模型往往会多走好几轮Token 消耗直接翻倍。所以我会在系统提示词里写清“当且仅当你已经输出最终结果时才返回 finish”。环境感知容易被忽视。Agent 拿到的不只是用户发来的一句话还包括当前时间、用户身份、业务上下文、上次任务的残留状态。感知层是 Agent 的输入边界要在进入主循环前统一封装不要允许模型随意思考到边界外。比如会议纪要助手的感知应该是“用户原始转写文本 当前日期 参会人列表”而不是让模型去猜时间。规划拆解是七要素里的核心难点。真正可落地的 Agent 不会让模型一次性从目标跳到最终结果而是把目标拆成若干步。工程上可以是一个TaskStateMachine每一步记录pending / running / done / failed。模型负责生成步骤状态机负责推进步骤两者配合才不容易跑飞。记忆管理和工具调用这些点后面七个决策点里我会展开讲。先记住一个原则七要素不是并列的七八个功能点它们是一条控制链。工程实现本质上就是把这条控制链稳定、可控、可观测地串起来。2. 七个决策点每个选择都是在花钱买确定性七要素解决的是“Agent 需要哪些零件”真正动手写代码时你还会遇到一系列取舍。我称之为七个决策点。之所以强调“决策”是因为每个选择都有明确的成本和收益没有标准答案只有适合场景的答案。这些决策点越早想清楚后面返工越少。2.1 模型选型先定推理上限再谈优化和降本很多人选模型只看榜单分数但要真正跑 Agent至少要关注五个维度指令遵循能力、函数调用稳定性、上下文长度、响应延迟、单次调用成本。其中函数调用稳定性可以这样测准备 20 个典型工具调用场景让模型输出符合你 schema 的结构化 JSON统计一次解析成功的比例。我实测下来不同模型在这项上的差距远大于在通用问答上的差距。参数上可以做一个简单的计算。假设你的 Agent 每轮任务平均需要 8 次工具调用模型 A 的函数调用成功率是 95%模型 B 是 70%。表面看差距不大但 8 次调用全部成功的概率A 是 0.95 的 8 次方大约 66%B 是 0.7 的 8 次方只有 5.7%。也就是说B 在实际任务中几乎很难从头到尾不重试。这就是为什么选型不能只看“谁说话聪明”。延迟也要单独测。Agent 是串行循环一次任务要多次调用模型。如果单次响应要 4 秒8 次调用就是 32 秒起步用户早就没耐心了。我一般会在选型阶段做一张表记录模型、单轮延迟、单次成本、20 个函数调用的成功率、支持的最大上下文。先定推理上限后续的优化和降本才有抓手。提示不要在生产环境把模型切换当成纯配置项至少留出一个影子环境做回归评测。模型升级或者降级函数调用行为都会变尤其要注意这类隐性变化。2.2 编排架构单链路、多 Agent、还是人机协同第二个决策点是架构形态。目前主流的 Agent 架构无非三种单 Agent、多 Agent、人机协同。单 Agent 就是“一个模型 一堆工具 一段循环”简单直接适合任务边界清晰、工具数量在 20 个以内的小型自动化场景。多 Agent 是把不同角色拆成多个模型实例比如一个“规划者”只负责拆任务一个“执行者”只负责调工具一个“审查者”只负责结果校验。它的优点是并行、可以让每个 Agent 的指令更聚焦代价是状态同步复杂、Token 消耗可能翻倍。很多人一上来就想搭多 Agent 天团结果链路一长日志都看不懂。我的建议是先跑通单 Agent当出现以下信号再拆分单个系统提示词超过 2000 字、工具超过 20 个、任务需要不同领域知识协作。否则单 Agent 的维护成本最低也最容易排查问题。人机协同也是被低估的选项。不是所有步骤都需要机器自动做把高风险操作设计成“Agent 生成建议人来点击确认”反而能更快上线。决策点不是“Agent 能不能全自动”而是“哪些环节能安全地自动化”。我做过一个自动发消息的项目最终上线版本就是所有外发消息都走人工确认这才敢让它在生产环境跑。2.3 记忆策略短期 Buffer、任务状态、长期知识库的分配记忆是 Agent 工程最容易被做坏的一层。很多团队直接把所有历史对话塞进 messages 数组最后 Token 爆掉。合理的记忆至少分三层短期记忆是当前轮次的工作区通常是一个messages数组保留最近 4~8 轮对话一定要有长度上限。工作记忆是正在执行的任务状态比如“当前步骤 3 / 5已完成文件上传下一步等待确认”以结构化字段存在内存或 Redis 中而不是存在自然语言里。长期记忆才是外部存储可以是一个向量数据库也可以是一张摘要表。长期记忆别盲目上向量库。如果你的场景是查“上次会议结论是什么”用 SQLite 存一张摘要表按日期和任务 ID 查询几十毫秒返回效果不比向量检索差。向量库适合语义召回比如“找一份和这份方案类似的文档”这时才需要 embedding。我在轻量项目里常用sqlite-vss在重量级场景才接独立的向量服务。记忆更新也要有纪律不是每次工具结果都值得写进长期记忆只有在任务完成或者出现明确结论时才写入。写入前做一次摘要而不是把原始往返过程全部存下来。否则长期记忆会变成垃圾堆查出来的东西根本不能用于决策。我给这个原则起名叫“记忆入库前先提炼”它能让 Agent 在长时间运行后仍然保持稳定。2.4 工具调用协议Function Calling 的 Schema、校验与容错工具层是 Agent 的“手”。最常见的失败来自 Function Calling 的 Schema 设计不严谨。比如参数描述模糊模型把日期格式传成“明天”或者没有定义枚举模型自创状态值。工程上要做到每个参数明确类型、必填、取值范围、示例值描述里写清业务口径。拿日期字段来说schema 里要写“格式 YYYY-MM-DD例如 2025-01-20”而不是只写“日期”。工具注册表建议集中管理。一个工具就是一个函数但入参和出参都要做一层封装。封装内部负责记录调用时间、参数、结果、异常捕获异常后把错误信息转换为模型能读的“观察文本”。例如工具超时返回给模型的不应该是堆栈而是“查询超时请稍后重试或改用其他关键字”。模型看到的是业务提示不是一堆英文报错。我还会在工具执行前做一层参数校验不直接信任模型的输出。明明category只能是“工作/生活/学习”模型传了“其他”那就立即返回校验失败而不是让业务代码去处理脏数据。校验失败也算一次观察模型看到后会重新生成。工具权限也很重要涉及删除、发送消息这类操作默认走人工确认这比任何提示词约束都可靠。2.5 上下文管理Token 是硬约束裁剪要讲策略先解释一个热搜词AI Agent token 是什么意思。Token 是模型处理文本的最小单位可以粗略理解成一个“字”。Agent 每次调用模型都要把系统提示、历史、工具结果拼进去长度直接影响成本和速度。上下文窗口是硬约束超出后模型会截断效果断崖。理解了这一点再看上下文管理本质上就是在有限的 Token 里塞进信息密度更高的内容。上下文裁剪最忌讳简单“掐头去尾”。很多人的做法是只保留最后几轮消息结果把系统提示和任务状态也丢了模型越跑越迷。我一般把上下文拆成四个区固定系统提示区严格控制字数任务状态区放当前目标、已完成步骤、下一步计划工具区只放当前步骤可能用到的工具的 schema而不是全部工具历史区用滑动窗口保留最近几轮更早的内容先做摘要。控制 Token 的另一个技巧是“结果摘要”。工具返回几百行数据不要直接塞给模型而是先调用一个摘要函数把结果压成两三句话。比如查询数据库返回 50 行摘要成“共 50 条记录其中 12 条状态为待办最近一条是今天上午 X”。模型拿这个信息做决策完全够用。这个习惯能让同样的上下文窗口承载更长的任务链路也是成本优化最有效的手段之一。2.6 任务规划状态机兜底模型只做动态决策Agent 要不要自己规划是另一个决策点。全自由规划看起来很美好但线上稳定性差。更稳的组合是“状态机兜底 模型做动态选择”。你要处理的任务流程如果是相对固定的比如“查资料 - 分析 - 生成报告”那就先用状态机把这三个阶段串起来。模型在每个阶段内部选择调用哪个工具、如何解读结果而不是决定整个流程走向。如果流程本身不确定再考虑 ReAct 式的动态决策也就是“思考 - 行动 - 观察”循环。ReAct 实现简单把思考过程写进 prompt 即可但它的问题是没有强制步骤模型容易反复走回头路。Plan-and-Execute 是另一种选择先让模型输出完整计划再按计划执行适合任务跨度大、需要提前看到全局的场景但问题在于计划可能与现实不匹配需要执行中重新规划。工程落地时我会在 Agent 代码里设置max_steps比如 8 步同时记录每一步的工具名和参数。一旦模型连续 3 步没有产生新信息就强制停止并触发应急预案。任务规划不是让模型越自由越好而是给它一个边界在边界内做决策。这样既能保留模型灵活性又能避免不可控的飘逸行为。2.7 可观测性与评测没有数据就不算工程最后但绝对不可省的是可观测性。Agent 的行为是概率性的必须有日志、追踪和评测。至少记录每次模型调用的输入片段长度、输出、Token 数、耗时、工具名、参数、结果、异常、重试次数、进入 Agent 时的目标和退出时的状态。有了这些问题排查才不是猜。我见过太多团队等到线上出问题才补日志结果 Agent 复现不了只能干瞪眼。评测是另一件容易被省略的事。我给每个 Agent 准备一个回归集不用太大30~50 条典型输入就够。每做完一次改动跑一遍回归。评测有两种脚本断言适合有确定结果的比如“调用了某个工具且参数正确”LLM 裁判适合开放结果比如“会议纪要是否包含结论”。我习惯双轨一起用先脚本断言卡硬指标再用 LLM 裁判评估质量。可观测性和评测不是上线后才补的而是写第一行 Agent 代码时就要一起写。后面第 4 章的问题清单里你会看到很多诡异故障都是靠日志和评测集定位的。没有这些基础设施你只能在“碰运气”和“猜”之间反复横跳。3. 实操过程用七要素加七个决策搭一个能用的 Agent 骨架理论说多了容易飘我拿一个最小但完整的例子走一遍。为了让思路不受语言限制我先说结论这套骨架不限定语言如果你追求极致的并发性能完全可以用 Rust 写 Agent runtime很多高性能服务的实践都是这么做的。但大多数团队起点在 Python所以下面代码我统一用 Python 表达你迁移到其他语言只是语法差异核心循环是一样的。3.1 明确需求做一个“会议纪要与任务跟进助手”这个例子足够小但能覆盖 Agent 的大部分工程问题。需求是输入一段会议录音转写文本Agent 要输出结构化会议纪要并把其中的待办任务写入任务系统下次用户问“我上周有哪些任务”时能从任务系统查出结果。套到七要素上目标是“完成纪要和任务入库”感知是用户输入的转写文本和当前日期规划是先摘要、再提取任务、再写入系统记忆是保存最近几次会议摘要工具是save_tasks和query_tasks行动是实际写入和查询反馈是写入成功后的校验。七要素全会用上而且每一项都有明确的代码落点不会为了演示而造概念。3.2 Agent 主循环最小的感知-决策-执行闭环直接看代码。逻辑很简单模型每轮输出一个结构化动作finish表示完成tool表示调用工具工具结果再放回记忆进入下一轮决策。import json from dataclasses import dataclass, field from typing import Callable, Dict, List, Any dataclass class AgentConfig: max_steps: int 8 system_prompt: str ( 你是一个会议助手。你需要根据用户输入输出会议纪要 并使用工具保存/查询任务。每次回复必须是一个 JSON 格式为{type: tool, name: ..., args: {...}} 或{type: finish, answer: ...} ) tools: Dict[str, Callable] field(default_factorydict) class MemoryBuffer: def __init__(self, size: int 6): self.messages: List[Dict[str, str]] [] self.size size def add(self, message: Dict[str, str]) - None: self.messages.append(message) if len(self.messages) self.size: self.messages.pop(0) def snapshot(self) - List[Dict[str, str]]: return list(self.messages) class Agent: def __init__(self, config: AgentConfig): self.config config self.memory MemoryBuffer() self.trace [] def call_llm(self, messages: List[Dict[str, str]]) - str: # 替换成真实模型调用返回字符串 raise NotImplementedError def parse_action(self, text: str) - Dict[str, Any]: # 容错解析剥离 markdown 代码块后 json.loads cleaned text.strip() if cleaned.startswith(json): cleaned cleaned.split(\n, 1)[1].rsplit(, 1)[0].strip() return json.loads(cleaned) def execute_tool(self, name: str, args: Dict[str, Any]) - str: if name not in self.config.tools: return f工具不存在: {name} try: result self.config.tools[name](**args) return str(result) except Exception as exc: return f工具执行失败: {exc} def run(self, user_input: str) - Dict[str, Any]: self.memory.add({role: user, content: user_input}) for step in range(self.config.max_steps): messages [{role: system, content: self.config.system_prompt}] self.memory.snapshot() llm_text self.call_llm(messages) self.memory.add({role: assistant, content: llm_text}) self.trace.append({step: step, llm: llm_text}) action self.parse_action(llm_text) if action[type] finish: return {status: success, answer: action[answer], trace: self.trace} if action[type] tool: observation self.execute_tool(action[name], action[args]) self.trace[-1][observation] observation self.memory.add({role: tool, content: observation}) else: return {status: failed, error: 未知 action 类型, trace: self.trace} return {status: failed, error: f超过最大步骤 {self.config.max_steps}, trace: self.trace}这段代码短但它包含了 Agent 主循环的完整链路感知通过user_input进入 memory决策由call_llm完成工具执行在execute_tool里工具结果作为 observation 回到 memory循环直到模型输出finish。没有复杂架构但足够开始调试。真实模型接入时只需要把call_llm替换成具体 SDK再在parse_action里做更健壮的解析即可。3.3 工具注册和记忆落地让 Agent 真正能“干活”主循环只是为了跑通流程真正让 Agent 有用的是工具层。我建议用装饰器把工具和 schema 绑在一起集中管理。下面是一个简化示范tools_registry: Dict[str, Dict[str, Any]] {} def register_tool(name: str, description: str, parameters: dict): def decorator(func: Callable): tools_registry[name] {func: func, description: description, parameters: parameters} return func return decorator register_tool( namesave_tasks, description把待办任务批量写入任务系统, parameters{ type: object, properties: { tasks: { type: array, items: { type: object, properties: { title: {type: string, description: 任务标题}, due_date: {type: string, description: 截止日期格式 YYYY-MM-DD} }, required: [title, due_date] } } }, required: [tasks] } ) def save_tasks(tasks: List[Dict[str, str]]) - str: # 这里替换成数据库写入 return f已保存 {len(tasks)} 条任务 register_tool( namequery_tasks, description按日期范围查询任务列表, parameters{ type: object, properties: { start_date: {type: string, description: 开始日期}, end_date: {type: string, description: 结束日期} }, required: [start_date, end_date] } ) def query_tasks(start_date: str, end_date: str) - str: # 替换成数据库查询 return 2024-01-01 ~ 2024-01-07 共 5 条任务其中 2 条未完成工具声明有两个关键点description 要写业务口径比如 due_date 是“截止日期”而不是“日期”parameters 里每个字段都要有示例因为模型是参考 schema 生成 JSON。如果 schema 写得含糊模型给你传{tasks: 买牛奶}这种错误结构一点也不意外。生产环境里可以把这些 schema 自动拼进系统提示词只放当前任务可能用到的部分进一步省 Token。记忆落地方面用上面的MemoryBuffer当短期记忆足够跑 Demo。生产环境建议把 Buffer 升级成 Redis 按session_id隔离再给每条消息打时间戳。长期记忆在轻量场景里用一张 MySQL 表保存会议摘要和结论查询时先按会议 ID 精确匹配不行再做全文检索。部署的时候用一个容器跑 Agent 服务按session_id动态创建 Agent 实例任务结束就释放避免内存泄露。3.4 稳定运行的三个细节解析修复、失败重试、步骤上限代码里parse_action目前只会剥一个代码块。实际模型可能输出中文引号、多余逗号、截断的 JSON。我的经验是做三级修复先用正则把常见错误替换掉比如中文冒号、中文逗号换成英文再用括号配对检查如果缺右括号就补上最后实在解析不了把“格式错误请重新输出 JSON”作为消息返回给模型重试。因为 Agent 循环里有max_steps重试次数天然受限不会无限烧钱。失败重试要区分模型层失败和工具层失败。模型层失败比如返回非 JSON走上面的修复重试。工具层失败要先把异常转成业务错误信息。例如写入任务时数据库连接断了返回给模型“连接失败请稍后重试”比直接把 Traceback 丢给模型更有效。模型读不懂堆栈它只需要知道下一步可以怎么做。步骤上限要结合任务类型设置。我一般先设 8 步跑一周看日志里的平均步数再设置成平均值的 2 倍。步数设太小会把正常任务截断设太大又会让死循环烧更多 Token。这个值不是拍脑袋定的而是靠可观测性数据持续校准。等任务模式稳定下来再对它做个性化调整。4. 常见问题与排查技巧实录4.1 工具调用死循环同一个工具翻来覆去执行这是我见过最多的故障。现象Agent 反复调用同一个工具参数还一样模型每次都按同一个错误结论继续。原因有两类一是工具返回的信息不足以让模型做出下一步判断模型只能重复试二是模型已经掉进了“重复 action”的惯性缺乏外部约束。排查顺序是看 trace 里每次工具返回的 observation。如果 observation 是“查询无结果”那问题出在工具返回信息设计上要改成返回“无结果建议换个关键词或检查筛选条件”如果 observation 每次都成功但模型仍然重复调用那要在 system prompt 里加一句“你已经调用过这个工具如果结果没有变化不要再次调用同一个工具”。代码层面还可以统计同一工具连续调用超过 N 次强制改走人工处理或直接结束。4.2 JSON 解析失败模型输出不是合法结构化数据现象模型返回了正常文字但你的json.loads直接报错。原因很多返回里带了 markdown 代码块、注释、中文标点甚至把引号写成了弯引号。处理方法是三步先提取代码块再做 normalize 替换常见错误最后如果还失败就把错误信息拼进下一次模型调用的 user 消息让它重新生成。还有一类情况是模型在 JSON 里写了注释。JSON 标准不允许注释所以你得在解析前把注释行删掉。更稳的做法是在 system prompt 里强调“不要加代码块、不要加注释、不要加任何解释只输出 JSON”。即使这样解析修复依然要有。不要把“模型一定会输出合法 JSON”当成假设要当成一个需要兜底的环节。4.3 上下文越长越“蠢”历史信息污染决策现象Agent 跑了几十轮后反而忘记早期目标开始回答无关内容。原因往往是上下文中塞进了太多中间观察和旧摘要模型注意力被稀释。解决思路是三层清理对话历史只保留最近 4 轮工具输出在写入 memory 前先压缩系统提示和工具 schema 保持不变。具体操作是把每次工具返回按长度阈值判断超过 200 字就先调用一次摘要只把摘要放进上下文。旧对话中的关键结论可以抽到“任务状态区”比如“已完成步骤上传文件当前步骤等待确认”不重要的寒暄直接丢。上下文管理的本质是把信息密度提升而不是无脑扩窗口扩到 100 万也一样会被无效信息填满。4.4 并发状态串线多个任务复用同一个实例现象A 用户的任务还没跑完B 用户发起请求后A 的工具调用参数里出现了 B 的数据。原因很明显Agent 实例里的 memory 是共享的没有按会话隔离。我在代码里强调过 MemoryBuffer 必须绑定 session_id。正确的做法是每次请求创建新的 Agent 实例或者用一个 session_id 到 Agent 的字典管理任务结束就释放实例。另一个隐形问题是工具函数闭包里的全局变量。比如任务系统客户端是单例但查询参数却写在全局变量里并发一高就会串。工具函数应该只接收调用参数所有业务对象都通过上下文注入并且尽量做到无状态。这块在代码评审时要重点看表面上单测都能过并发压测才会暴露。下面是一个排查速查表我贴在团队文档里当第一级排查手册现象大概率原因建议处理反复调用同一工具工具观察信息不足增强工具返回的业务提示偶发 JSON 解析失败模型输出格式不稳定加解析修复和重试上下文越长效果越差历史数据过多摘要 滑动窗口并发任务数据串了Agent 实例共享按 session_id 隔离最后说一个我做 Agent 工程落地最大的体会模型负责聪明工程负责稳定。你不需要把每个 Agent 都做成全自动多智能体只要把七要素接到代码、七个决策点落实到流程、日志和评测能跟上一个能上线的 Agent 就已经跑起来了。再分享一个小技巧每次上线前我会故意往评测集里塞几条边界输入比如空字符串、重复命令、超长文本这些地方往往比正常路径更容易暴露问题。希望这篇文章能帮你少踩几个坑。
返回列表