AI Agent工程实践:七要素拆解与七个关键决策点
1. 为什么现在聊 Agent 不能只聊概念过去一年我参与过三个从零到一的 Agent 项目也帮朋友救过两个已经半死不活的 Agent 系统。最大的感受是市面上讲 Agent 的文章90% 都在讲Agent 是什么——能自主规划、能调用工具、能记忆、能反思配一张循环流程图完事。但真正落到工程实现你会发现这些概念描述几乎帮不上忙。你打开 IDE面对的第一个问题不是Agent 要不要有记忆而是这个循环到底该用 while 还是事件驱动、工具调用的返回结果超过上下文窗口了怎么办、模型连续三次调用同一个工具失败我该在哪一层拦截。这就是我想写这篇东西的原因。标题里说的七要素和七个决策点不是我从某篇论文里抄来的框架而是我在实际搭建和调试 Agent 的过程中反复踩坑之后总结出来的一套拆解方式。七要素回答的是一个 Agent 系统由哪些部分组成七个决策点回答的是在工程实现时你必须在哪些地方做出选择以及每个选择背后的代价是什么。这篇文章适合谁看如果你已经用过大模型 API写过简单的 prompt 调用想进一步搭建一个能真正跑起来的 Agent 系统那这篇就是写给你的。如果你已经在做 Agent 开发但总觉得系统能跑但不可靠那七个决策点部分可能会让你重新审视一些之前忽略的环节。我不会只讲应该怎么做而是会讲我为什么这么选以及选错了会怎样。核心关键词先摆出来AI Agent、LLM、工具调用、循环机制。这四个词贯穿全文也是理解 Agent 工程实现的四条主线。下面我从整体设计思路开始拆。2. Agent 系统的七要素拆解2.1 七要素不是架构图是检查清单很多人第一次接触 Agent 架构看到的是 LangChain 或 Dify 那种框图用户输入 → LLM → 工具 → LLM → 输出。这个图没错但它太粗了。粗到你在实现时根本不知道从哪里下手。我习惯把 Agent 系统拆成七个要素每个要素对应一个具体的工程模块要素工程对应核心问题目标任务定义与终止条件Agent 什么时候算做完了感知输入解析与上下文构建模型看到的信息从哪来规划任务分解与步骤生成复杂任务怎么拆成可执行步骤工具外部能力接口Agent 能做什么、不能做什么记忆短期与长期状态管理跨轮次的信息怎么保留执行循环控制与调度谁来决定下一步做什么反思结果校验与纠错做错了怎么发现、怎么修这七个要素里目标和执行是最容易被低估的。大多数人把精力花在工具和规划上结果系统跑起来之后发现Agent 不知道什么时候停或者停得太早。我见过一个客服 Agent用户问帮我查一下订单它查完订单之后又主动去查了物流、又去查了优惠券最后输出一大段用户根本没要的信息。问题出在哪出在目标定义不清晰终止条件没有和任务绑定。2.2 目标与终止条件Agent 的刹车比油门重要一个 Agent 系统的目标定义至少要回答三个问题第一用户输入的任务边界在哪里第二什么状态下可以认为任务完成第三什么状态下必须放弃或转人工。我在实际项目里通常会把终止条件写成显式的规则而不是完全交给模型判断。比如class TaskGoal: def __init__(self, description, max_steps10, success_criteriaNone): self.description description self.max_steps max_steps self.success_criteria success_criteria or [] self.step_count 0 def is_complete(self, state): if self.step_count self.max_steps: return max_steps_reached for criterion in self.success_criteria: if not criterion(state): return None return success def should_abort(self, state): if state.get(consecutive_failures, 0) 3: return True return False这段代码看起来简单但它解决了一个很实际的问题模型有时候会过度热情。你让它查天气它查完天气之后还顺便给你推荐了穿衣建议、出行建议、空气质量分析。如果没有max_steps和success_criteria双重约束这个循环可能一直跑下去。实操心得终止条件一定要在系统层面硬编码不要指望模型自己判断我做完了。模型对完成的理解和你的业务定义经常不一致。2.3 感知与上下文构建模型看到什么决定它做什么感知这个要素在工程上就是上下文构建。用户输入一句话你不能直接把这句话扔给模型。你需要决定要不要带上历史对话、要不要带上工具列表、要不要带上系统提示词、要不要带上检索到的知识。我踩过最大的坑是上下文窗口的分配。早期做的一个 Agent系统提示词写了 2000 token工具描述占了 1500 token历史对话保留了最近 10 轮结果真正留给用户当前输入和模型思考的空间只剩不到 2000 token。模型经常忘记用户刚才说了什么因为历史对话被截断了。后来我改成动态分配系统提示词压缩到 800 token 以内工具描述按需加载只加载当前任务可能用到的工具历史对话用摘要代替原文。这样上下文窗口的利用率从 40% 提升到了 75% 左右。def build_context(user_input, history, tools, max_tokens8000): system_prompt load_system_prompt() # 控制在 800 token relevant_tools select_tools(user_input, tools) # 按需选择 tool_desc format_tools(relevant_tools) # 控制在 1000 token history_summary summarize_history(history) # 摘要而非原文 remaining max_tokens - count_tokens(system_prompt tool_desc history_summary) user_context truncate(user_input, remaining) return system_prompt tool_desc history_summary user_context这个函数的核心逻辑是先保证系统提示词和工具描述的空间再用剩余空间装历史摘要和用户输入。顺序不能反否则模型可能连自己有哪些工具都不知道。2.4 规划与工具拆得开才调得动规划和工具这两个要素在实际工程里是耦合的。因为规划的结果往往就是下一步调用哪个工具。我见过两种极端一种是规划做得特别重每次都要生成完整的任务分解树另一种是完全没有规划模型直接根据当前状态决定下一步。我的经验是简单任务不需要显式规划复杂任务必须有规划但不能太重。判断标准很简单——如果任务可以用一句话描述清楚比如查一下北京今天的天气那就不需要规划直接让模型决定调用天气工具就行。如果任务是帮我分析一下上个月的销售数据找出异常点并生成一份报告那就需要规划因为步骤多、依赖关系复杂。规划的实现方式我推荐轻量级步骤列表而不是完整任务树def plan_task(task_description, available_tools): prompt f任务{task_description} 可用工具{available_tools} 请生成一个不超过5步的执行计划每步格式为 - 步骤描述 | 需要的工具 | 预期输出 只输出计划不要执行。 plan llm_call(prompt) return parse_plan(plan)为什么限制 5 步因为超过 5 步的计划模型在执行过程中大概率会偏离。与其生成一个 10 步的完美计划然后执行到第 3 步就乱了不如生成一个 5 步的粗略计划执行完再重新规划。2.5 记忆与反思让 Agent 记住教训而不是重复犯错记忆和反思是七要素里最虚的两个但工程上其实很实在。记忆就是状态管理反思就是结果校验。记忆我分两层短期记忆用对话历史 当前任务状态存在内存里长期记忆用向量数据库存关键事实和用户偏好跨会话保留。短期记忆的关键是什么时候压缩长期记忆的关键是什么时候写入。反思我通常做成一个独立的校验步骤而不是让模型自己想一想。比如工具调用返回结果之后用一个轻量级的校验函数判断结果是否合理def validate_tool_result(tool_name, result, expected_format): if result is None: return False, 工具返回空结果 if not matches_format(result, expected_format): return False, f结果格式不符合预期{expected_format} if is_suspicious(result): return False, 结果数值异常可能调用失败 return True, ok这个校验函数不依赖模型纯规则判断。规则判断能解决的问题不要交给模型。模型应该用在它擅长的地方——理解语义、生成文本、做模糊决策。2.6 执行循环Agent 的心脏执行循环是七要素里最核心的工程部分也是七个决策点的主要战场。一个典型的 Agent 循环长这样def agent_loop(task, tools, max_iterations15): state init_state(task) for i in range(max_iterations): context build_context(state) action llm_decide(context, tools) if action.type finish: return action.result if action.type tool_call: result execute_tool(action.tool_name, action.params) state update_state(state, action, result) if should_abort(state): return handle_abort(state) return handle_max_iterations(state)这个循环看起来简单但每个环节都有决策点。max_iterations设多少llm_decide的 prompt 怎么写execute_tool失败了怎么办should_abort的判断条件是什么这些就是下一章要展开的七个决策点。3. 七个决策点Agent 工程实现的关键选择3.1 决策点一循环用同步还是异步这是你写第一行 Agent 代码时就要面对的问题。同步循环写起来简单for循环里依次调用 LLM 和工具逻辑清晰。但问题是如果工具调用耗时很长比如调用一个外部 API 要 3 秒整个循环就被阻塞了。异步循环用asyncio或者事件驱动可以并发执行多个工具调用。但复杂度直线上升你需要处理并发状态更新、超时控制、异常传播。我的选择标准是如果工具调用平均耗时超过 1 秒或者单次任务可能涉及 3 个以上工具调用就用异步。否则同步足够。import asyncio async def async_agent_loop(task, tools, max_iterations15): state init_state(task) for i in range(max_iterations): context build_context(state) action await llm_decide_async(context, tools) if action.type finish: return action.result if action.type tool_call: try: result await asyncio.wait_for( execute_tool_async(action.tool_name, action.params), timeout10.0 ) except asyncio.TimeoutError: result {error: tool_timeout} state update_state(state, action, result) return handle_max_iterations(state)注意asyncio.wait_for的超时控制。没有超时控制的异步循环比同步循环更危险因为一个卡住的工具调用会让整个 Agent 挂起。3.2 决策点二工具调用失败后重试几次工具调用失败是常态不是异常。网络抖动、API 限流、参数格式错误都会导致失败。问题是重试几次每次重试之间等多久我的经验值是最多重试 2 次第一次立即重试第二次等 1 秒之后不再重试把错误信息返回给模型让它决定下一步。def execute_tool_with_retry(tool_name, params, max_retries2): for attempt in range(max_retries 1): try: result call_tool(tool_name, params) if is_valid_result(result): return result except Exception as e: if attempt max_retries: return {error: str(e), attempts: attempt 1} time.sleep(1.0 if attempt 1 else 0) return {error: max_retries_exceeded}为什么是 2 次而不是 3 次或 5 次因为超过 2 次的重试大概率不是临时故障而是参数本身有问题。这时候继续重试只是浪费 token 和时间不如把错误信息交给模型让它修正参数或者换一个工具。注意事项重试逻辑一定要记录日志。我见过一个 Agent 因为工具重试没有日志线上跑了三天才发现某个工具一直在失败模型一直在用错误结果做决策。3.3 决策点三上下文超限时截断还是摘要上下文窗口是硬约束。当对话历史 工具结果 当前输入超过窗口限制时你必须做选择截断丢掉最早的内容还是摘要用模型压缩历史。截断的优点是快、便宜、不引入额外模型调用。缺点是可能丢掉关键信息。摘要的优点是保留语义缺点是慢、贵、摘要本身可能失真。我的做法是混合策略最近 3 轮对话保留原文更早的对话做摘要工具结果只保留最近 5 条更早的工具结果只保留摘要。def compress_context(history, tool_results, max_tokens): recent_history history[-3:] # 最近3轮保留原文 older_history history[:-3] history_summary summarize(older_history) if older_history else recent_tools tool_results[-5:] older_tools tool_results[:-5] tool_summary summarize_tools(older_tools) if older_tools else return recent_history, history_summary, recent_tools, tool_summary这个策略的核心逻辑是越近的信息越重要越具体的信息越重要。最近一轮的工具调用结果可能直接影响下一步决策必须保留原文。而三轮之前的对话用户大概率已经不关心细节了摘要足够。3.4 决策点四模型选择——大模型还是小模型Agent 系统里通常不止一个模型调用。规划用哪个模型工具选择用哪个模型结果校验用哪个模型全部用最大的模型成本扛不住全部用最小的模型效果扛不住。我的分配策略是环节模型选择理由任务规划大模型需要理解复杂意图小模型容易跑偏工具选择中等模型主要是分类任务中等模型足够参数生成中等模型需要一定的格式化能力结果校验小模型或规则大部分校验可以用规则少量用模型最终输出大模型直接影响用户体验不能省这个分配不是绝对的但原则是影响决策方向的环节用大模型影响执行细节的环节可以用小模型纯格式校验用规则。3.5 决策点五工具描述写多详细工具描述是模型选择工具的唯一依据。写得太简单模型不知道什么时候该用写得太详细占用大量上下文窗口。我的经验是每个工具的描述控制在 50-100 token包含四个要素工具名、功能一句话、参数列表、一个使用示例。{ name: search_order, description: 根据订单号查询订单状态。适用于用户询问订单进度、物流信息时。, parameters: { order_id: string, 订单号通常为12位数字 }, example: search_order(order_id123456789012) }注意description里的适用于...这句话。这句话直接告诉模型什么时候该用这个工具比单纯描述功能有效得多。我做过对比测试加上适用场景描述之后工具选择的准确率提升了大约 20%。3.6 决策点六循环终止条件怎么设终止条件是 Agent 的刹车。设得太松Agent 会过度执行设得太紧Agent 会半途而废。我通常设三层终止条件成功终止模型输出finish动作且结果通过校验失败终止连续 3 次工具调用失败或连续 2 次模型输出无法解析硬终止达到最大迭代次数通常 10-15 次或总耗时超过阈值通常 60 秒def should_terminate(state): if state.finish_requested and validate_final_result(state): return success if state.consecutive_failures 3: return failure if state.unparseable_outputs 2: return failure if state.iteration state.max_iterations: return max_iterations if time.time() - state.start_time 60: return timeout return None这三层条件里硬终止是最重要的。因为前两层依赖模型输出而模型有时候会陷入死循环——反复调用同一个工具、反复输出同样的内容。硬终止是最后的保险。3.7 决策点七日志与可观测性做到什么程度Agent 系统的调试难度远高于普通程序因为它的行为不是确定性的。同样的输入两次运行可能走不同的路径。没有完善的日志你根本不知道问题出在哪。我的日志策略是记录每个决策点的输入和输出def log_decision(step, decision_type, input_data, output_data, metadataNone): log_entry { timestamp: time.time(), step: step, type: decision_type, input: truncate_for_log(input_data), output: truncate_for_log(output_data), metadata: metadata or {} } logger.info(json.dumps(log_entry, ensure_asciiFalse))关键是要记录模型看到了什么input、模型决定了什么output、工具返回了什么metadata。这三样凑齐大部分问题都能复现和定位。实操心得日志里一定要记录 token 消耗。我见过一个 Agent 因为某次工具返回结果特别长导致后续每次循环都带着这个长结果token 消耗暴涨 10 倍。如果日志里有 token 记录这个问题当天就能发现。4. 从零搭建一个最小可用 Agent 的完整流程4.1 环境准备与依赖选择搭建 Agent 不需要复杂的框架。我的建议是第一版不要用 LangChain 或 Dify用原生 Python 大模型 API 写。原因很简单框架帮你做了太多决策你反而不知道问题出在哪。等你把原生版本跑通了再考虑用框架提效。依赖只需要三个pip install openai # 或任何大模型 SDK pip install httpx # 用于工具调用的 HTTP 请求 pip install pydantic # 用于参数校验如果你要用异步再加asyncio标准库自带。向量数据库第一版可以不用用简单的字典或 JSON 文件存长期记忆就够。4.2 定义工具接口与注册机制工具是 Agent 的手。第一版建议只注册 3-5 个工具太多了模型选择困难。from pydantic import BaseModel from typing import Callable, Any class Tool(BaseModel): name: str description: str parameters: dict func: Callable class ToolRegistry: def __init__(self): self.tools {} def register(self, tool: Tool): self.tools[tool.name] tool def get(self, name: str) - Tool: return self.tools.get(name) def list_descriptions(self) - str: return \n.join([ f- {t.name}: {t.description}\n 参数: {t.parameters} for t in self.tools.values() ])注册工具的时候description要写清楚什么时候用parameters要写清楚类型和格式。这两点直接决定模型能不能正确调用。4.3 编写 Agent 主循环主循环是整个系统的核心。我把它拆成四个函数build_prompt、call_llm、parse_action、execute_action。def build_prompt(task, state, tool_descriptions): return f你是一个任务执行 Agent。当前任务{task} 可用工具 {tool_descriptions} 当前状态 - 已完成步骤{state.completed_steps} - 最近工具结果{state.last_tool_result} 请决定下一步动作。输出格式 {{action: tool_call, tool: 工具名, params: {{...}}}} 或 {{action: finish, result: 最终结果}} 只输出 JSON不要其他内容。 def call_llm(prompt): response client.chat.completions.create( modelgpt-4o-mini, messages[{role: user, content: prompt}], temperature0 ) return response.choices[0].message.content def parse_action(llm_output): try: return json.loads(llm_output) except json.JSONDecodeError: return {action: error, message: 无法解析模型输出} def execute_action(action, registry): if action[action] finish: return action[result] if action[action] tool_call: tool registry.get(action[tool]) if not tool: return {error: f工具 {action[tool]} 不存在} return tool.func(**action[params]) return {error: 未知动作类型}temperature0很重要。Agent 的决策需要稳定性不需要创造性。温度调高会让模型在相同输入下输出不同结果增加调试难度。4.4 接入第一个真实工具第一个工具建议选一个简单、稳定、无副作用的 API。比如查询天气、查询时间、或者查询一个公开的数据库。import httpx def get_weather(city: str) - dict: 查询城市天气 try: resp httpx.get( fhttps://api.example.com/weather, params{city: city}, timeout5.0 ) return resp.json() except Exception as e: return {error: str(e)} weather_tool Tool( nameget_weather, description查询指定城市的当前天气。适用于用户询问天气、温度、是否下雨时。, parameters{city: string, 城市名称如北京}, funcget_weather ) registry.register(weather_tool)注意timeout5.0。工具调用一定要设超时否则一个慢 API 会拖垮整个 Agent。4.5 跑通第一个完整任务把上面的代码串起来跑一个查询北京天气并给出穿衣建议的任务def run_agent(task): state init_state(task) for i in range(10): prompt build_prompt(task, state, registry.list_descriptions()) llm_output call_llm(prompt) action parse_action(llm_output) if action[action] finish: return action[result] result execute_action(action, registry) state.update(action, result) return 达到最大迭代次数 result run_agent(查询北京今天的天气并给出穿衣建议) print(result)预期执行路径模型先调用get_weather(city北京)拿到天气数据后再输出finish动作包含穿衣建议。如果模型第一次就输出finish而没有调用工具说明 prompt 里的工具描述不够清晰需要调整。4.6 加入记忆与状态持久化第一版跑通之后加入简单的状态持久化。用 JSON 文件存每次任务的状态方便调试和恢复。import json def save_state(task_id, state): with open(fstates/{task_id}.json, w) as f: json.dump(state.to_dict(), f, ensure_asciiFalse) def load_state(task_id): with open(fstates/{task_id}.json) as f: return State.from_dict(json.load(f))这个简单的持久化机制在调试时非常有用。你可以看到 Agent 每一步的状态变化定位问题比看日志更直观。5. 常见问题与排查技巧实录5.1 模型不调用工具直接输出答案这是最常见的问题。原因通常有三个工具描述不够清晰、prompt 没有强调必须使用工具、模型本身能力不足。排查顺序先检查工具描述里有没有写适用于...再检查 prompt 里有没有明确要求如果需要外部信息必须调用工具最后换一个更强的模型测试。# 在 prompt 里加入强制要求 prompt \n注意如果任务需要实时信息或外部数据你必须调用工具获取不要凭记忆回答。5.2 工具调用参数格式错误模型生成的参数格式和工具期望的不一致比如该传字符串的传了数字该传数组的传了字符串。解决方法是在工具描述里把参数格式写死并在执行前做校验def validate_params(tool, params): for key, expected_type in tool.parameters.items(): if key not in params: return False, f缺少参数 {key} if not isinstance(params[key], expected_type): return False, f参数 {key} 类型错误期望 {expected_type} return True, ok5.3 Agent 陷入死循环模型反复调用同一个工具或者反复输出同样的内容。这是最危险的问题因为会持续消耗 token。排查看日志里连续几步的action是否相同。如果是检查工具返回结果是否让模型困惑——比如工具一直返回错误模型一直重试。解决在状态里记录连续相同动作的次数超过阈值就强制终止。if state.last_action action and state.same_action_count 2: return 检测到重复动作终止执行5.4 上下文超限导致模型失忆模型突然忘记了之前的信息或者开始胡言乱语。这通常是上下文被截断导致的。排查在日志里记录每次调用的 token 数看是否接近模型上限。解决启用前面说的混合压缩策略最近 3 轮保留原文更早的做摘要。5.5 工具返回结果过长某个工具返回了一大段 JSON 或 HTML占满了上下文窗口。解决在工具层面做结果裁剪只返回模型需要的关键字段。def trim_tool_result(result, max_length500): if isinstance(result, str) and len(result) max_length: return result[:max_length] ...(已截断) if isinstance(result, dict): return {k: trim_tool_result(v, max_length // len(result)) for k, v in result.items()} return result5.6 常见问题速查表问题现象可能原因排查方法解决方向不调用工具描述不清/prompt 未强调检查工具描述和 prompt补充适用场景强制要求参数格式错误类型未约束打印模型输出参数校验 描述写死格式死循环工具返回异常/模型困惑看连续动作日志重复动作检测 强制终止上下文超限历史或工具结果过长记录 token 数混合压缩策略工具超时API 慢/网络问题记录工具耗时超时控制 重试结果不准确模型能力不足对比不同模型关键环节换大模型6. 一些关于 Agent 工程实现的个人体会做 Agent 这一年多最大的体会是Agent 的难点不在模型在工程。模型能力每年都在提升但工程问题——循环控制、状态管理、错误处理、上下文压缩——这些不会因为模型变强而自动消失。反而模型越强你越需要精细的工程控制来发挥它的能力。另一个体会是不要追求全自动。我早期总想让 Agent 完全自主地完成所有任务结果发现可靠性根本达不到要求。后来改成关键决策点人工确认的模式比如工具调用前让用户确认参数或者最终输出前让用户审核可靠性立刻上了一个台阶。Agent 的价值不是替代人而是把人从重复劳动中解放出来让人专注于需要判断力的环节。最后一个建议从最小可用版本开始逐步加功能。不要一上来就搞多 Agent 协作、长期记忆、复杂规划。先把单 Agent 3 个工具 简单循环跑通跑稳再考虑扩展。我见过太多项目架构设计得很宏大结果连最基本的工具调用都没跑通。这个内容后续还可以这样扩展加入多 Agent 协作的通信机制、加入基于向量数据库的长期记忆、加入工具调用的权限控制和安全审计。但那是下一步的事先把单 Agent 跑稳再说。