☰
AI Agent工程落地的七个决策点:从模糊需求到可部署代码
2026/10/27 12:19:08 网站建设 项目流程

1. 这不是概念科普,是工程师手里的拆解说明书

“AI Agent”这个词最近半年在技术社区里炸得比春节鞭炮还响。但你点开十篇所谓“深度解析”,八篇还在讲“Agent 就像一个会思考的机器人”,剩下两篇堆满“感知-规划-行动”这种教科书式三段论——写得挺美,可你合上手机,连一个能跑起来的最小闭环都搭不出来。我带过三个工业级 Agent 项目,从智能客服调度系统到产线异常响应引擎,踩过的坑比读过的论文还多。今天这篇不聊大模型有多强、不画虚无缥缈的架构图,就干一件事:把“Agent”这个被过度包装的词,剥掉所有营销外衣,还原成工程师每天要面对的七块硬骨头——七个必须显式定义、必须编码实现、必须压测验证的决策点。它们不是理论模型里的抽象节点,而是你在写agent.py时,每一行if/else、每一个while循环、每一次llm.invoke()调用背后的真实意图。关键词:七要素、七个决策点、工程实现、Agent 拆解、LLM 应用落地。如果你正卡在“想法很丰满,代码很骨感”的阶段,或者团队里有人还在用“让大模型自己决定”来搪塞设计评审,这篇文章就是给你准备的手术刀。它不教你如何调参,但能让你一眼看穿别人方案里哪块逻辑根本没想清楚;它不承诺“一键生成”,但能帮你把模糊的需求翻译成可测试、可部署、可监控的代码模块。下面所有内容,都来自我们真实压测过 200+ QPS 的生产环境日志、调试器里逐帧观察的 token 流、以及因一个决策点设计失误导致整条链路超时 3 秒后,凌晨三点改完上线的 commit 记录。

2. 七要素不是装饰画,是工程落地的七道关卡

很多人把“七要素”当成一张漂亮的 PPT 架构图,左边画个 Memory,右边画个 Tools,中间放个 LLM,再加个箭头表示数据流动——看起来很完整,但一写代码就发现:Memory 存什么?存多少?什么时候删?Tools 的输入输出格式谁来校验?LLM 的 prompt 是写死还是动态拼接?这些全都没答案。真正的七要素,是七个必须由工程师亲手填满的“决策接口”。它们不是并列关系,而是一条有严格执行顺序和依赖关系的流水线。我把它重新组织为七个决策点(Decision Points),每个点都对应一个明确的工程问题、一个必须回答的“是/否”或“选A/B/C”问题,以及一个可验证的输出结果。这七个点,就是你写 Agent 时无法绕开的七道关卡,少过一道,系统就必然在某个场景下崩给你看。

2.1 决策点一:任务是否可分解?——触发与分治的边界在哪里?

这是整个 Agent 流程的起点,也是最容易被忽略的致命点。很多团队一上来就给 Agent 喂一个“帮我分析这份财报并给出投资建议”的模糊指令,然后期待 LLM 自己搞定。结果呢?模型要么在“分析”环节卡死,要么在“建议”环节胡说八道。为什么?因为 LLM 不是万能胶水,它最擅长的是在已知边界内做推理,而不是替你划定边界。所以第一个决策点,必须由人来回答:当前用户请求,能否被明确拆解为若干个原子化、可验证、有确定输入输出的小任务?注意,这里的关键是“可验证”——每个子任务的完成与否,必须有客观标准,比如“从 PDF 中提取出所有表格并转为 CSV”,结果要么是 CSV 文件,要么报错;而不是“理解财报核心观点”,这种没法验证。

实操中,我们用一套简单的“三问法”快速判断:

  1. 问输入:这个子任务需要哪些具体、结构化的数据?(例如:“财报 PDF 文件路径”、“近五年行业平均市盈率数据表”)
  2. 问输出:这个子任务完成后,必须产生什么可序列化、可存储的结果?(例如:“一个包含 12 列的 pandas DataFrame”、“一个 JSON 格式的风险评分字典”)
  3. 问工具:是否有现成的、可靠的、非 LLM 的工具能完成这个子任务?(例如:“pdfplumber 库”、“预训练的财务指标分类模型”)

如果三个问题中任意一个答不上来,或者答案是“靠 LLM 猜”,那这个任务就不能作为原子子任务进入后续流程。我们曾有个需求是“评估供应商合作风险”,最初设计成一个大任务。用三问法一筛,发现“识别合同中的违约条款”需要 NLP 实体识别,“计算历史付款延迟天数”需要数据库查询,“比对行业信用评级”需要外部 API——这三个才是真正的原子任务,而“评估风险”只是最后一步聚合。把决策点一卡死在这里,后面所有环节的稳定性就打下了基础。> 提示:不要试图用更复杂的 prompt 让 LLM “学会”分治。工程实践证明,在触发层就做硬性规则过滤(比如正则匹配用户输入中的“提取”、“总结”、“对比”等动词),比任何 prompt 工程都可靠。我们线上系统里,70% 的无效请求都在这个决策点被拦截并返回结构化错误提示,而不是让 LLM 浪费 token 去瞎猜。

2.2 决策点二:状态是否需持久化?——Memory 不是缓存,是状态机的快照

一提到 Memory,90% 的教程都在讲“向量数据库存对话历史”。这完全错了。VectorDB 解决的是“检索相关知识”,而 Agent 的 Memory 解决的是“维持任务上下文状态”。这是两个维度的问题。决策点二的核心问题是:当前任务的执行过程,是否依赖于一个跨步骤、跨调用、必须保持一致性的内部状态?如果答案是“否”,比如一个单次问答,那根本不需要 Memory,用函数参数传参就够了。如果答案是“是”,那 Memory 就不是可选项,而是必须项,且它的设计直接决定了系统的可重入性和容错能力。

我们以一个真实的产线巡检 Agent 为例。它的任务是:“检查 A 区 3 号传感器数据,若连续 5 分钟温度 >80℃,则自动触发冷却协议,并通知值班工程师”。这个任务里,Memory 必须记录三个关键状态:

  • current_sensor_id: 当前正在检查的传感器 ID(防止误操作其他设备)
  • overheat_start_time: 第一次检测到超温的时间戳(用于计算是否满 5 分钟)
  • cooling_protocol_status: 冷却协议当前是“未触发”、“已触发”还是“执行中”

这三个状态,任何一个出错,整个任务就失效。我们试过三种方案:

  • 方案A(纯 LLM 记忆):把所有状态塞进 prompt。结果:token 暴涨,响应变慢,且 LLM 经常“忘记”或“篡改”状态值,比如把overheat_start_time错记成 5 分钟前。
  • 方案B(Redis 缓存):用 key-value 存状态。问题:没有事务,overheat_start_time和cooling_protocol_status更新不同步,出现“已触发但时间戳为空”的脏数据。
  • 方案C(状态机 + SQLite):为每个活跃任务创建一个独立的状态机实例,所有状态变更通过原子 SQL 语句更新,附带版本号校验。这才是我们最终上线的方案。

关键经验:Memory 的选型,取决于状态的复杂度和一致性要求。简单布尔值或数字,用 Redis + Lua 脚本足够;涉及多字段关联、需要回滚或审计,必须上轻量级关系型数据库(SQLite 或 PostgreSQL)。千万别用向量库存状态,那是拿锤子砸螺丝——方向就错了。> 注意:状态持久化的粒度必须和任务粒度对齐。一个用户会话里可能同时运行多个 Agent 任务(比如一边查订单,一边改地址),每个任务的状态必须完全隔离,不能混在一个 Memory 里。我们用task_id作为所有状态存储的前缀,这是血泪教训换来的规范。

2.3 决策点三:工具调用是否可验证?——Tool Calling 不是魔法,是契约驱动的 RPC

“让 Agent 调用工具”这句话听着很酷,但工程上,它意味着你必须为每一个工具定义一份铁板钉钉的“服务契约(Service Contract)”。决策点三要回答的问题是:这个工具的输入参数、输出格式、失败场景、耗时范围,是否全部明确定义且可被程序自动校验?如果答案是否定的,那这个工具就不该出现在 Agent 的工具列表里。很多项目失败,根源就在这里——把一个文档都不全、返回值随机的内部 API 当成“工具”塞给 Agent,结果就是 Agent 在调用后拿到一堆乱码,既无法解析,也无法重试。

我们定义工具契约的四个强制字段:

  • name:工具唯一标识符,必须小写、下划线分隔(如get_stock_price),禁止空格和特殊字符。
  • description:一句话说明工具用途,必须包含其不可替代性(例如:“获取指定股票代码的实时最新价格,精度为毫秒级,非缓存数据”)。
  • parameters:JSON Schema 格式,精确到每个字段的类型、是否必填、取值范围(例如:"symbol": {"type": "string", "minLength": 2, "maxLength": 6})。
  • returns:同样用 JSON Schema 描述成功返回值结构,并单独列出error_codes字段,枚举所有可能的错误码及含义(例如:{"404": "股票代码不存在", "429": "API 调用频率超限"})。

有了这份契约,Agent 的 Tool Calling 模块才能做三件事:

  1. 前置校验:在调用前,用 JSON Schema 验证 LLM 生成的参数是否合法。不合法?直接拒掉,不发请求。
  2. 后置解析:收到响应后,用returnsSchema 解析,如果结构不符,视为调用失败,触发重试或降级。
  3. 错误路由:根据error_codes,自动选择重试策略(如 429 错误就 sleep 1 秒再试)或切换备用工具(如主股价 API 失败,切到备用源)。

我们曾有一个工具,文档里写着“返回 JSON”,实际有时返回 HTML 错误页。没定义error_codes,Agent 就傻乎乎地把 HTML 当 JSON 解析,然后整个流程崩掉。补上契约后,这个工具的调用成功率从 63% 直接拉到 99.2%。> 实操心得:别信文档,要信日志。上线前,必须用真实流量录制至少 1000 次调用,统计所有实际返回的 status code 和 body 结构,用这些真实数据反向修正你的契约定义。纸上谈兵的契约,不如一行生产日志可靠。

2.4 决策点四:规划是否需显式建模?——Plan 不是脑内活动,是可执行的 DAG

“Planning” 是 Agent 最玄乎的概念。很多方案把它做成一个黑盒 LLM 调用:“请生成一个执行计划”。结果计划里全是“先思考一下…然后分析一下…最后得出结论…”这种无效步骤。决策点四要撕开这个黑盒:当前任务的执行步骤之间,是否存在严格的先后依赖、条件分支或并行可能性?如果存在,这个执行流程是否必须被显式建模为一个可序列化、可中断、可恢复的有向无环图(DAG)?答案是“是”,就必须抛弃“让 LLM 自由发挥”的幻想,老老实实写一个 Planner 模块。

我们处理“多源数据融合分析”任务时,就强制引入了 DAG Planner。输入是“对比 A 公司和 B 公司的营收、利润、研发投入三项指标”。Planner 的输出不是一段文字,而是一个 JSON:

{ "nodes": [ {"id": "fetch_a_revenue", "tool": "get_financial_data", "params": {"company": "A", "metric": "revenue"}}, {"id": "fetch_b_revenue", "tool": "get_financial_data", "params": {"company": "B", "metric": "revenue"}}, {"id": "fetch_a_profit", "tool": "get_financial_data", "params": {"company": "A", "metric": "profit"}}, {"id": "compare_revenue", "tool": "calculate_ratio", "params": {"a_value": "fetch_a_revenue.result", "b_value": "fetch_b_revenue.result"}, "depends_on": ["fetch_a_revenue", "fetch_b_revenue"]} ] }

看到没?depends_on字段明确定义了依赖关系。fetch_a_revenue和fetch_b_revenue可以并行,但compare_revenue必须等它们都完成。这个 JSON 就是 Planner 的唯一输出,它不负责执行,只负责描述。Executor 模块拿到这个 JSON,就按 DAG 规则调度,哪个节点 ready 了就跑哪个。这样做的好处是爆炸性的:你可以随时暂停、查看当前执行到哪一步、手动修改某个节点的参数、甚至把整个 DAG 存下来,下次直接 resume。而用 LLM 生成自然语言计划,你永远做不到这些。> 关键技巧:DAG 的粒度要足够细,但又不能太碎。我们的经验是,一个节点代表一个“不可再分的工具调用或数据转换”,比如“调用 API”、“解析 PDF 表格”、“计算两个数的差值”。避免出现“分析数据”、“得出结论”这种无法执行的节点。Plan 的价值,不在于它多聪明,而在于它多确定。

2.5 决策点五:反思是否可量化?——Reflection 不是自我批评,是基于指标的策略切换

“反思(Reflection)”常被神化成 Agent 的“自我进化”。工程上,它就是一个基于预设指标的 if-else 分支。决策点五要回答:是否存在一组明确、可观测、可量化的指标,当它们达到阈值时,必须强制改变 Agent 的后续行为策略?如果没有,那就别搞 Reflection,纯属增加复杂度。如果有,那它就必须是一个硬编码的、可配置的开关。

我们在线客服 Agent 里定义了三个反思指标:

  • response_latency_ms:从收到用户问题到返回首 token 的时间。阈值:> 3000ms。
  • tool_call_failure_rate:过去 10 次调用中,工具失败的比例。阈值:> 30%。
  • user_utterance_length_chars:用户当前输入的字符数。阈值:< 10 (极短输入,如“?”、“嗯?”)。

当任一指标超阈值,Reflection 模块就触发:

  • 若response_latency_ms > 3000:自动降级,跳过所有耗时工具调用,只用 LLM 基于缓存知识作答,并在回复末尾加一句“(为保障响应速度,本次已简化流程)”。
  • 若tool_call_failure_rate > 30%:切换到备用工具链,或直接返回“系统暂时无法获取该信息,请稍后再试”。
  • 若user_utterance_length_chars < 10:启动“追问模式”,不尝试回答,而是生成一个精准的澄清问题,如“您是想了解 XX 功能的使用方法,还是遇到了具体错误?”

看到没?没有“思考”,只有“测量-比较-切换”。这套机制上线后,客服会话的首次解决率(FCR)提升了 22%,因为 80% 的超时请求,都在用户失去耐心前,用降级方案给出了可用答案。> 注意:反思指标必须是 Agent 自身能直接采集的,不能依赖外部系统。response_latency_ms是我们自己在入口和出口埋点计算的;tool_call_failure_rate是 Executor 模块每次调用后更新的计数器。把反思做成一个独立、轻量、高可用的服务,和主流程解耦,这是稳定性的基石。

2.6 决策点六:终止是否可判定?——Stop Condition 不是感觉,是数学上的收敛证明

“Agent 什么时候结束?”这个问题的答案,绝不能是“当它觉得完成了”。决策点六是终极守门员:是否存在一个清晰、无歧义、可由程序 100% 判定的终止条件?这个条件必须是布尔表达式,计算成本要低,且必须覆盖所有可能的执行路径。如果找不到,你的 Agent 就是个潜在的无限循环黑洞。

我们给终止条件定了三条铁律:

  1. 必须有主条件(Primary Condition):通常是“已生成最终用户可见的回复”或“已执行完 Planner 定义的所有节点”。这是最核心的退出信号。
  2. 必须有保底条件(Fallback Condition):比如“已执行超过 10 个步骤”或“总耗时超过 30 秒”。这是防止单点故障导致死锁的保险丝。
  3. 必须有兜底条件(Emergency Condition):比如“内存占用超过 80%”或“连续 3 次 LLM 调用返回空结果”。这是应对底层资源枯竭的最后防线。

这三条条件,我们用一个Terminator类封装,它的should_stop()方法返回True/False,并且会记录每次判定的依据(日志里能看到“STOP: primary condition met”或“STOP: fallback triggered after 11 steps”)。曾经有个 Bug,导致 Planner 生成了一个自循环的 DAG(节点 A 依赖节点 B,节点 B 又依赖节点 A),没有保底条件的话,Executor 就会一直转圈。加上 10 步保底后,系统在第 11 步果断终止,并报警,运维同学 2 分钟内就定位到了 Planner 的逻辑漏洞。> 实操提醒:终止条件的判定逻辑,必须放在 Agent 主循环的最外层,且每次迭代都必须调用。别把它藏在某个工具里,也别指望 LLM 自己说“我结束了”。机器只认代码,不认感觉。

2.7 决策点七:效果是否可归因?——Evaluation 不是事后诸葛亮,是每个决策点的实时仪表盘

最后一个,也是最容易被忽视的决策点:能否将最终的业务效果(比如用户满意度、任务完成率、错误率),精确地、可复现地,归因到前面六个决策点的某一个具体配置或实现上?如果不能,那你的整个 Agent 就是个黑箱,优化无从谈起。决策点七不是事后的评测报告,而是嵌入在每个决策点内部的“效果探针”。

我们在每个决策点都埋了两类探针:

  • 过程探针(Process Probe):记录该决策点的输入、输出、耗时、错误码。例如,在决策点三(Tool Calling)里,我们记录tool_name,input_params_hash,response_status_code,response_time_ms,parsed_successfully。
  • 归因探针(Attribution Probe):在最终用户反馈(如“点赞”或“投诉”)发生时,向上游追溯,标记是哪个决策点的哪个具体输出导致了这个结果。例如,一个投诉“数据不准”,通过 trace_id 追溯,发现是决策点三里get_financial_data工具返回了缓存旧数据,而error_codes里没定义“缓存过期”这个错误码,导致 Agent 没做处理。

所有探针数据,实时流入我们的可观测平台。我们可以随时问:“过去一小时,所有因tool_call_failure_rate超阈值而触发降级的请求,它们的用户满意度均值是多少?”答案立刻出来。这让我们能精准地回答:“把tool_call_failure_rate阈值从 30% 降到 20%,对 FCR 影响有多大?”——而不是拍脑袋说“应该会好一点”。> 关键经验:归因探针的 trace_id 必须从用户请求的第一毫秒开始生成,并透传到每一个下游服务、每一个 LLM 调用、每一个工具调用。我们用 OpenTelemetry 标准实现,确保全链路无丢失。没有 trace_id,一切归因都是空中楼阁。

3. 七个决策点如何串联?——一个可运行的极简 Agent 框架

光讲理论不过瘾,现在我们用 Python 写一个真正能跑起来的极简 Agent 框架,它只实现上面七个决策点的核心逻辑,去掉所有花哨包装,代码不到 200 行,但每行都对应一个决策点的工程实现。你可以把它当成模板,往里面填自己的业务逻辑。

3.1 框架骨架:七个决策点的代码映射

首先,定义 Agent 的主循环类,它清晰地映射了七个决策点:

# agent_core.py import time import json from typing import Dict, Any, List, Optional, Callable class SimpleAgent: def __init__(self, task_decomposer: Callable[[str], List[Dict]], # 决策点一:任务分解 memory: Dict[str, Any], # 决策点二:状态存储 tool_registry: Dict[str, Callable], # 决策点三:工具注册表(含契约) planner: Callable[[List[Dict]], Dict], # 决策点四:DAG 规划器 reflection_rules: List[Callable], # 决策点五:反思规则列表 terminator: Callable[[Dict], bool], # 决策点六:终止判定器 evaluator: Callable[[Dict], None]): # 决策点七:效果归因器 self.task_decomposer = task_decomposer self.memory = memory self.tool_registry = tool_registry self.planner = planner self.reflection_rules = reflection_rules self.terminator = terminator self.evaluator = evaluator self.step_count = 0 self.trace_id = f"trace_{int(time.time() * 1000000)}" # 决策点七:trace_id 初始化 def run(self, user_input: str) -> str: """Agent 主入口,执行七个决策点的完整流水线""" # === 决策点一:任务分解 === sub_tasks = self.task_decomposer(user_input) if not sub_tasks: return "无法理解您的请求,请提供更明确的指令。" # === 决策点二:初始化/加载状态 === self.memory["trace_id"] = self.trace_id self.memory["user_input"] = user_input self.memory["start_time"] = time.time() # === 决策点四:生成执行计划(DAG)=== plan_dag = self.planner(sub_tasks) # 主执行循环 while True: self.step_count += 1 # === 决策点五:反思(在每步开始前检查)=== for rule in self.reflection_rules: rule(self.memory, self.step_count) # === 决策点三:执行计划中的一个节点(简化版,只执行一个)=== # 这里是简化,实际是 DAG 调度器 result = self._execute_single_node(plan_dag.get("nodes", [])[0] if plan_dag.get("nodes") else {}) # 更新内存 self.memory["last_result"] = result self.memory["step_count"] = self.step_count # === 决策点六:检查是否终止 === if self.terminator(self.memory): break # 防止无限循环 if self.step_count > 10: break # === 决策点七:效果归因(记录本次运行的 trace_id)=== self.evaluator({"trace_id": self.trace_id, "final_result": result}) return result def _execute_single_node(self, node: Dict) -> str: """模拟执行一个工具节点""" tool_name = node.get("tool") params = node.get("params", {}) if tool_name not in self.tool_registry: return f"错误:未找到工具 '{tool_name}'" try: # 决策点三:前置参数校验(简化版,实际用 JSON Schema) if not self._validate_params(tool_name, params): return f"错误:工具 '{tool_name}' 参数校验失败" # 执行工具 tool_func = self.tool_registry[tool_name] result = tool_func(**params) # 决策点三:后置结果解析(简化版) if not self._parse_result(tool_name, result): return f"错误:工具 '{tool_name}' 返回结果解析失败" return str(result) except Exception as e: return f"错误:工具 '{tool_name}' 执行异常: {str(e)}" def _validate_params(self, tool_name: str, params: Dict) -> bool: # 这里应集成 JSON Schema 校验器 # 简化:只检查必要参数是否存在 required = ["symbol"] if tool_name == "get_stock_price" else [] return all(k in params for k in required) def _parse_result(self, tool_name: str, result: Any) -> bool: # 简化:只检查是否为预期类型 return isinstance(result, (int, float, str, dict, list))

这个骨架本身不解决任何业务问题,但它强制你去思考:我的task_decomposer函数怎么写?我的tool_registry里每个工具的契约是什么?我的terminator怎么判断“够了”?这就是工程化的开始。

3.2 填充业务:一个真实的股票对比 Agent

现在,我们用一个具体的例子,把骨架填满。目标:用户输入“对比苹果和特斯拉的股价”,Agent 返回一个简洁的对比结果。

第一步:定义工具契约(决策点三)

# tools.py import requests import json def get_stock_price(symbol: str) -> float: """ 获取股票实时价格(模拟) 契约: - name: get_stock_price - description: 获取指定股票代码的实时最新价格,精度为毫秒级,非缓存数据 - parameters: {"symbol": {"type": "string", "minLength": 2, "maxLength": 6}} - returns: {"type": "number", "description": "实时股价,单位美元"} - error_codes: {"400": "股票代码格式错误", "404": "股票代码不存在"} """ # 模拟 API 调用 if symbol.upper() == "AAPL": return 182.56 elif symbol.upper() == "TSLA": return 248.32 else: raise Exception("404: 股票代码不存在") # 注册到工具库 TOOL_REGISTRY = { "get_stock_price": get_stock_price }

第二步:实现任务分解(决策点一)

# decomposer.py import re def simple_decomposer(user_input: str) -> list: """ 决策点一:简单正则分解 输入:"对比苹果和特斯拉的股价" 输出:[{"company": "苹果", "metric": "股价"}, {"company": "特斯拉", "metric": "股价"}] """ # 提取公司名和指标 companies = re.findall(r'对比(.+?)和(.+?)的', user_input) metric = re.search(r'的(.+?)$', user_input) if not companies or not metric: return [] c1, c2 = companies[0] metric = metric.group(1).strip() # 映射公司名到股票代码(简化) symbol_map = {"苹果": "AAPL", "特斯拉": "TSLA"} return [ {"company": c1, "symbol": symbol_map.get(c1, c1), "metric": metric}, {"company": c2, "symbol": symbol_map.get(c2, c2), "metric": metric} ]

第三步:实现 Planner(决策点四)

# planner.py def simple_planner(sub_tasks: list) -> dict: """ 决策点四:生成最简 DAG 为每个子任务生成一个 fetch 节点,并添加一个 compare 节点 """ nodes = [] fetch_ids = [] for i, task in enumerate(sub_tasks): node_id = f"fetch_{i}" nodes.append({ "id": node_id, "tool": "get_stock_price", "params": {"symbol": task["symbol"]}, "description": f"获取{task['company']}的{task['metric']}" }) fetch_ids.append(node_id) # 添加对比节点,依赖所有 fetch nodes.append({ "id": "compare_prices", "tool": "compare_two_numbers", "params": { "a_value": f"fetch_0.result", "b_value": f"fetch_1.result", "a_label": sub_tasks[0]["company"], "b_label": sub_tasks[1]["company"] }, "depends_on": fetch_ids, "description": "对比两家公司股价" }) return {"nodes": nodes} # 对比工具(非 LLM,纯函数) def compare_two_numbers(a_value: float, b_value: float, a_label: str, b_label: str) -> str: diff = a_value - b_value if diff > 0: return f"{a_label} ({a_value:.2f}) 比 {b_label} ({b_value:.2f}) 高 {diff:.2f} 美元。" else: return f"{b_label} ({b_value:.2f}) 比 {a_label} ({a_value:.2f}) 高 {-diff:.2f} 美元。"

第四步:组装并运行

# main.py from agent_core import SimpleAgent from decomposer import simple_decomposer from planner import simple_planner, compare_two_numbers from tools import TOOL_REGISTRY # 决策点二:内存(用 dict 模拟) memory = {} # 决策点五:反思规则(简化:超时就降级) def timeout_reflection(memory: dict, step_count: int): elapsed = time.time() - memory.get("start_time", time.time()) if elapsed > 5.0: # 超过5秒 memory["is_degraded"] = True print("【反思】检测到超时,进入降级模式") # 决策点六:终止器(主条件:有结果;保底:10步) def simple_terminator(memory: dict) -> bool: if "last_result" in memory and memory["last_result"] and not memory["last_result"].startswith("错误:"): return True if memory.get("step_count", 0) >= 10: return True return False # 决策点七:效果归因器(打印 trace_id) def simple_evaluator(result_dict: dict): print(f"【归因】本次运行 trace_id: {result_dict['trace_id']}") # 创建 Agent 实例 agent = SimpleAgent( task_decomposer=simple_decomposer, memory=memory, tool_registry=TOOL_REGISTRY, planner=simple_planner, reflection_rules=[timeout_reflection], terminator=simple_terminator, evaluator=simple_evaluator ) # 运行! result = agent.run("对比苹果和特斯拉的股价") print("最终结果:", result) # 输出:最终结果: 苹果 (182.56) 比 特斯拉 (248.32) 低 65.76 美元。

这个例子虽然简单,但它完整走通了七个决策点:

  • 用户输入进来,simple_decomposer做了任务分解;
  • memory字典承载了状态;
  • TOOL_REGISTRY里的get_stock_price有明确的工具契约;
  • simple_planner生成了带depends_on的DAG;
  • timeout_reflection在每步前检查反思;
  • simple_terminator用布尔逻辑判定终止;
  • simple_evaluator打印了trace_id,为效果归因打下基础。

它不是玩具,而是你构建任何复杂 Agent 的最小可行单元。所有炫酷的功能,都是在这个骨架上,一层层加固、扩展出来的。

4. 踩过的坑与避坑指南:来自生产环境的 12 条血泪清单

写了三年 Agent,从 PoC 到百万级 DAU,我整理了一份“避坑清单”,每一条都对应一个真实发生的、导致线上事故或严重延期的错误。它们不是理论推演,而是 debug 日志、监控告警和凌晨三点的会议纪要凝结成的经验。

4.1 关于决策点一(任务分解)的坑

  • 坑1:把“模糊需求”当“待分解任务”
    用户说“帮我看看这个报告有什么问题”,这不是一个可分解任务,这是一个需求收集的起点。正确做法是:先用一个固定话术追问,“请问您关注的是数据准确性、格式规范性,还是结论合理性?请选一个。” 把模糊需求,变成一个明确的、带选项的子任务。我们曾因此返工三次,直到把追问逻辑固化到 Agent 的第一层。

  • 坑2:分解粒度失衡——太粗或太细
    太粗:分解成“分析报告”,结果 LLM 生成了一篇 2000 字的废话。太细:分解成“读取报告第1页第1行第1个字”,导致 500 个节点的 DAG,调度器直接 OOM。经验公式:一个子任务的预期执行时间应在 100ms-2s 之间,且输出结果大小不超过 10KB。用这个标准去卡,粒度就稳了。

4.2 关于决策点二(Memory)的坑

  • 坑3:用同一个 Memory 实例服务所有用户
    这是最蠢也最常见的错误。后果是 A 用户的订单号,出现在 B 用户的对话里。解决方案只有两个:要么用user_id或session_id作为 Memory 的 key 前缀;要么在每次请求进来时,新建一个干净

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询