1. 从“堆模块”到“做减法”:一次架构重构的完整复盘做Agent这一年来,我最大的感受就是:这个领域太容易让人产生“加东西就能变强”的幻觉了。每次效果不好,第一反应就是“是不是缺个记忆模块”“是不是该加个规划器”“要不要再挂一个工具路由层”。结果就是架构图越画越复杂,节点越连越多,但实际跑起来的任务完成率却卡在一个瓶颈上死活上不去。直到有一次,我狠下心把整个多层级架构删到只剩一个核心循环,效果反而出现了肉眼可见的提升。这不是玄学,也不是“少即是多”这种正确的废话,而是有具体的工程逻辑在里面的。这篇文章就把这次改动的来龙去脉拆开讲清楚,包括当时踩过的坑、删减的判断依据、删完之后怎么保证功能不丢,以及最终实测下来的效果对比。如果你也在做Agent相关的东西,不管是用现成框架还是自己手搓,这篇内容应该能帮你少走一些弯路。先交代一下背景。我做的Agent主要面向多步骤任务自动化场景,比如根据一段自然语言描述去操作一组内部工具、查询数据、生成结构化报告。这类任务的特点是步骤之间有依赖关系,中间状态需要传递,而且经常需要根据上一步的结果决定下一步做什么。一开始我的思路很“教科书”:既然任务复杂,那就把架构分层——顶层做任务分解,中间层做工具调度,底层做具体执行,再加一个记忆模块存上下文,一个反思模块做结果校验。听起来很合理对吧?但实际跑起来,问题一个接一个。最典型的问题是错误传播。顶层分解出来的子任务,到了中间层可能被重新解释,到了底层执行时又变了一次意思。三层下来,原始意图已经面目全非。另一个问题是延迟叠加。每个模块都要调一次模型,串行下来一个任务跑几十秒甚至几分钟,用户体验极差。还有一个隐性成本:调试困难。当最终结果不对时,你很难定位到底是哪一层的锅,因为每一层都在做“智能决策”,每一层都可能出错。所以当我决定“把架构删了”的时候,其实是在做一个很朴素的判断:与其让多个模块互相猜意图,不如让一个模块从头管到尾。这个判断背后有理论支撑,也有工程上的权衡,下面慢慢展开。2. 为什么“加模块”反而让Agent变笨了2.1 多层级架构的隐性成本:信息损耗与意图漂移很多人做Agent的第一反应是模仿软件工程里的分层架构,觉得这样“解耦”“可维护”。但Agent和传统软件有一个本质区别:传统软件的模块之间传递的是确定的数据结构,而Agent模块之间传递的是自然语言。自然语言本身就是有损压缩,每经过一层“理解-重新表达”,信息就会丢失一部分。我举个实际例子。用户输入是:“帮我查一下上个月华东区销售额超过50万的客户,把他们的联系方式整理成表格,然后给每个客户发一封问候邮件。”这是一个典型的复合任务。在我的旧架构里,顶层规划器会把它拆成三个子任务:查询、整理、发邮件。但顶层规划器在拆解时,可能会把“华东区”这个条件漏掉,或者把“超过50万”理解成“排名前50”。然后中间层拿到这个已经失真的子任务,再去做工具选择,又可能选错工具。底层执行时,即使工具选对了,参数也可能传错。整个过程就像传话游戏,传到最后一个人,原话已经变了。更糟糕的是,每一层都在“自信地”做决策,没有任何一层会主动说“我不确定”。所以最终结果错了,你甚至不知道从哪一层开始错的。这里的关键认知是:Agent的每一层“智能”都是概率性的,概率性的叠加不是线性增长,而是误差累积。层数越多,最终输出的方差越大。2.2 延迟与成本:每个模块都是一次模型调用除了信息损耗,多层级架构还有一个硬伤:延迟。每多一个模块,就多一次模型调用。如果每个模块平均耗时2秒,五个模块串行就是10秒。这还没算工具调用的时间。对于交互式场景,超过5秒用户就开始不耐烦了;对于批量任务,延迟直接转化为成本。我实测过旧架构在一个典型任务上的耗时分布:顶层规划1.8秒,中间调度2.1秒,工具执行平均3.5秒,反思校验2.3秒,记忆更新0.9秒。总计约10.6秒。而删减后的单循环架构,同样的任务平均耗时4.2秒。性能提升了一倍多,而且任务完成率还更高了。成本方面更直观。旧架构每次任务平均消耗约4500个token(因为每个模块都要把上下文重新喂一遍),新架构约1800个token。按大规模调用来算,这是数倍的差距。2.3 调试噩梦:当每一层都在“智能决策”还有一个很少被提及但极其致命的问题:可观测性。在多层级架构里,每一层都在做“智能决策”,这意味着每一层的行为都是不确定的。你没法像调试传统代码那样打断点、看变量。你只能看日志,但日志里全是自然语言,而且每一层的自然语言表达风格还不一样。我印象最深的一次排查:一个任务连续失败了五次,每次失败的原因都不一样。第一次是规划器漏了一个条件,第二次是调度器选错了工具,第三次是执行器参数格式不对,第四次是反思模块误判了结果,第五次是记忆模块把错误信息覆盖了正确信息。五个模块,每个都有“犯错”的能力,组合起来就是灾难。所以当我决定删架构的时候,核心动机就是:把不确定性收敛到一个地方。让一个模块负责从理解到执行的全过程,这样出错时我只需要看一个地方的日志,调整一个地方的提示词,优化一个地方的逻辑。3. 删减策略:保留什么、砍掉什么、合并什么3.1 核心保留项:一个循环 + 一个工具集 + 一份状态删架构不是把东西全扔了,而是重新划分职责边界。我最终保留的东西只有三样:一个主循环:负责接收输入、决定下一步动作、执行动作、判断是否结束。这个循环本质上就是一个“思考-行动-观察”的循环,类似ReAct模式,但做了大量简化。一个工具集:所有可调用的工具都注册在一个扁平的工具列表里,没有分类、没有路由层。工具的描述信息直接写在提示词里,让模型自己选。一份状态:用一个结构化的JSON对象保存任务执行过程中的所有中间状态,包括已完成的步骤、当前步骤、待办事项、关键变量等。这份状态在每个循环迭代时都会被更新,并且完整地传给模型。就这三样。没有规划器、没有调度器、没有反思模块、没有独立的记忆模块。所有这些东西的功能,都被合并到了主循环的提示词和状态管理里。3.2 砍掉规划器:让模型在循环中“边做边想”规划器是我第一个砍掉的模块。原因很简单:在任务开始时就制定完整计划,本质上是在信息不完整的情况下做决策。用户的需求可能模糊,工具的执行结果可能出乎意料,环境状态可能变化。一个在第一步就定死的计划,到了第五步很可能已经完全不适用了。砍掉规划器之后,我改成让模型在每一轮循环中只决定“下一步做什么”。这听起来像是退回到了短视的贪心策略,但实际效果反而更好。因为每一轮循环时,模型都能看到最新的状态(包括上一步的执行结果),所以它的决策是基于最新信息的。这就像开车时看着前方调整方向,而不是出发前就把方向盘焊死。当然,这要求状态设计得非常清晰。模型需要知道:目标是什么、已经做了什么、当前处于什么阶段、有哪些工具可用。这些信息都在提示词里明确给出,模型就能做出合理的下一步决策。3.3 砍掉独立记忆模块:状态即记忆独立的记忆模块也是我早期架构里的一个“标配”。它的职责是存储历史交互、检索相关记忆、更新记忆库。但实际用下来,我发现对于大多数任务型Agent来说,记忆根本不需要独立的模块。为什么?因为任务型Agent的“记忆”本质上就是当前任务的执行状态。用户让你查数据、整理表格、发邮件,这个过程中的所有信息都是围绕当前任务的。你不需要从三个月前的对话里检索什么,你只需要记住这个任务已经执行到哪一步了。所以我把记忆模块砍了,改成一份结构化的状态对象。这份状态在每个循环迭代时都会被完整地传给模型,模型自己决定哪些信息重要、哪些可以忽略。这比让一个独立的记忆模块去“智能检索”要可靠得多,因为检索本身就可能漏掉关键信息。3.4 合并反思与执行:在循环中自然校验反思模块是我最后砍掉的。它的初衷是好的:在任务完成后检查结果是否正确。但问题是,反思模块和主循环看到的信息是一样的,它并没有额外的信息优势。如果主循环都判断不了结果对不对,反思模块凭什么能判断?所以我改成在循环内部做校验。具体来说,每次工具执行后,模型会看到执行结果,然后判断:这个结果是否符合预期?是否需要重试?是否需要调整参数?如果一切正常,就继续下一步;如果异常,就进入错误处理分支。这本质上就是把反思融入了循环,而不是把它作为一个独立的阶段。这样做的好处是校验是即时的。传统架构里,反思是在任务结束后才做的,这时候错误已经传播了很久。而在循环内校验,错误可以在发生的当下就被发现和处理。4. 单循环架构的具体实现与关键细节4.1 主循环的伪代码与状态结构设计先看主循环的核心逻辑。我用的是Python风格的伪代码,实际实现可以是任何语言:def run_agent(user_input, tools, max_iterations=20): state = { "goal": user_input, "completed_steps": [], "current_step": None, "pending_steps": [], "variables": {}, "last_error": None, "iteration": 0 } while state["iteration"] < max_iterations: state["iteration"] += 1 # 构建提示词,包含目标、状态、工具描述 prompt = build_prompt(state, tools) # 模型决策:返回下一步动作 action = model.decide(prompt) # 判断是否结束 if action.type == "finish": return action.result # 执行工具 try: result = execute_tool(action.tool_name, action.parameters) state["completed_steps"].append({ "step": action.description, "tool": action.tool_name, "result": result }) state["last_error"] = None except Exception as e: state["last_error"] = str(e) # 不中断循环,让模型决定如何处理错误 # 更新变量 if action.updates: state["variables"].update(action.updates) return {"error": "达到最大迭代次数"}这个循环看起来很简单,但有几个关键设计点:第一,状态是显式传递的。每一轮循环都把完整状态传给模型,模型不需要“记住”什么,所有信息都在提示词里。这消除了对记忆模块的依赖。第二,错误不中断循环。工具执行失败时,错误信息被写入状态,然后继续下一轮循环。模型会看到错误信息,并决定是重试、换工具、还是调整参数。这比直接抛异常要健壮得多。第三,有最大迭代次数限制。这是防止无限循环的安全阀。实际使用中,大多数任务在5到10轮内完成,20轮足够覆盖复杂任务。4.2 提示词工程:如何让模型在循环中不迷路单循环架构对提示词的要求比多模块架构更高,因为所有决策都集中在一个地方。我的提示词模板经过多次迭代,最终稳定在以下结构:你是一个任务执行Agent。你的目标是用最少的步骤完成用户的任务。 ## 当前目标 {goal} ## 当前状态 - 已完成步骤:{completed_steps} - 当前变量:{variables} - 上一步错误:{last_error} ## 可用工具 {tool_descriptions} ## 决策规则 1. 每次只决定下一步做什么,不要规划多步。 2. 如果上一步有错误,优先处理错误。 3. 如果任务已完成,返回 finish。 4. 如果需要调用工具,明确指定工具名和参数。 5. 如果信息不足,调用工具获取信息,不要猜测。 ## 输出格式 {output_format}这个提示词有几个关键点:明确要求“每次只决定下一步”。这防止模型在单次决策中试图规划多步,导致输出过长且容易出错。错误处理优先级最高。如果上一步出错,模型会优先处理错误,而不是继续往下走。强调“不要猜测”。这是减少幻觉的关键。模型在信息不足时应该调用工具,而不是编造答案。输出格式严格约束。我用JSON schema约束输出,确保模型返回的是结构化的动作,而不是自由文本。实操心得:提示词里的“决策规则”部分不要写太多条,超过7条模型就开始忽略后面的。我试过写15条规则,结果模型只遵守前5条。后来精简到5条,遵守率明显提升。4.3 工具设计:扁平化注册与描述优化工具集的设计也经历了大改。旧架构里,工具是分类注册的,有“查询类”“操作类”“计算类”等。模型需要先选类别,再选具体工具。这多了一步决策,而且类别划分本身就可能模糊。新架构里,所有工具扁平注册,模型直接从列表里选。工具描述我做了大量优化,核心原则是:让模型一眼看懂这个工具是干什么的、什么时候该用、参数怎么传。一个典型的工具描述长这样:{ "name": "query_sales_data", "description": "查询销售数据。当用户需要获取销售额、客户信息、订单记录时使用此工具。", "parameters": { "region": "地区名称,如'华东'、'华南'。不传则查询全部地区。", "min_amount": "最小金额筛选,单位元。不传则不筛选。", "time_range": "时间范围,格式'YYYY-MM'。不传则查询当月。" }, "returns": "返回符合条件的记录列表,每条包含客户名、联系方式、销售额、日期。" }注意几个细节:description里明确写了“什么时候用”。这帮助模型在正确的场景下选择正确的工具。参数说明里写了“不传则...”。这减少了模型因为不确定参数而卡住的情况。returns里描述了返回结构。这让模型能预判执行结果,从而更好地决定下一步。我实测下来,工具描述的质量对任务完成率的影响非常大。同样的工具集,描述优化前后,任务完成率差了将近20个百分点。4.4 状态管理:用JSON Schema约束中间数据状态管理是单循环架构的基石。如果状态设计得不好,模型在循环中就会“迷路”。我的状态对象用JSON Schema严格约束,确保每个字段都有明确的类型和含义。{ "goal": "string, 用户原始输入", "completed_steps": [ { "step": "string, 步骤描述", "tool": "string, 使用的工具名", "result": "object, 工具返回结果", "timestamp": "string, ISO时间" } ], "variables": { "key": "value" }, "last_error": "string or null", "iteration": "integer" }关键设计点:completed_steps是数组,保留完整历史。模型可以看到每一步做了什么、结果是什么。这比只保留“当前状态”要可靠,因为模型可以回溯。variables是自由字典。用于存储任务执行过程中提取的关键信息,比如“客户列表”“筛选条件”等。模型可以往里面写,也可以从里面读。last_error单独字段。让模型一眼看到上一步是否出错,优先处理。注意事项:状态对象不要太大。我试过把完整的工具返回结果都塞进状态,结果提示词长度爆炸,模型反而抓不住重点。后来改成只保留关键字段,比如查询结果只保留前10条记录和总数,效果反而更好。5. 实测效果对比与典型场景分析5.1 任务完成率与延迟的量化对比我用了三组任务做对比测试,每组50个任务,覆盖简单查询、多步操作、异常处理三种类型。旧架构和新架构的对比结果如下:指标旧多层级架构新单循环架构变化简单查询完成率92%96%+4%多步操作完成率68%84%+16%异常处理完成率45%72%+27%平均延迟(简单)6.2秒2.8秒-55%平均延迟(多步)12.4秒5.1秒-59%平均token消耗42001750-58%多步操作和异常处理的提升最明显。这符合预期,因为这两类任务最容易受到信息损耗和错误传播的影响。单循环架构下,模型始终看到完整状态,决策基于最新信息,所以表现更稳定。5.2 一个多步任务的完整执行记录拿一个实际任务举例:“查一下上个月华东区销售额超过50万的客户,整理成表格,然后给每个客户发一封问候邮件。”旧架构的执行过程:规划器拆解:查询客户 → 整理表格 → 发邮件(漏掉了“华东区”和“50万”条件)调度器选择工具:query_customers(参数只有时间范围)执行器返回:所有客户列表(几百条)反思模块:结果太多,可能不对,但无法判断哪里不对最终输出:一个包含几百条记录的表格,任务失败新架构的执行过程:循环1:模型看到目标,决定调用query_sales_data,参数region=华东, min_amount=500000, time_range=上个月循环2:工具返回23条记录。模型判断需要整理成表格,调用format_table工具循环3:表格生成成功。模型判断需要发邮件,调用send_email工具,参数为23个客户的邮箱和问候模板循环4:邮件发送成功。模型返回finish整个过程4轮循环,耗时约4.5秒,任务完成。关键区别在于:模型在每一步都能看到完整的目标和最新的状态,所以不会漏条件、不会选错工具、不会误判结果。5.3 什么情况下单循环架构会吃力单循环架构不是万能的。我实测下来,它在以下场景会吃力:超长任务(超过20步):状态对象会变得很大,提示词长度爆炸,模型注意力分散。这种场景可能需要引入分层,但分层的方式要重新设计。需要并行执行的任务:单循环是串行的,如果任务中有多个独立子任务可以并行,单循环会浪费时间。不过大多数任务型Agent的场景都是串行的,所以这个问题不突出。需要长期记忆的任务:如果任务需要跨会话记住用户偏好,单循环架构需要额外设计持久化机制。但这和架构层数无关,是另一个维度的问题。所以我的建议是:先从单循环开始,遇到瓶颈再考虑加东西。不要一上来就搞复杂架构,那样你连问题出在哪都找不到。6. 常见问题与排查技巧实录6.1 模型在循环中“卡住”怎么办这是最常见的问题。模型反复调用同一个工具,或者反复返回相同的错误。排查思路:检查状态更新逻辑:是不是工具执行后状态没有正确更新?模型看到的状态和实际状态不一致,就会重复决策。检查工具返回:是不是工具返回了空结果或异常结果,但状态里没有体现?模型不知道失败了,就会一直重试。检查提示词:是不是没有明确告诉模型“如果连续两次相同操作,必须换策略”?我在提示词里加了一条规则:“如果上一步和当前步骤相同,必须改变策略或返回错误。”6.2 工具选择错误的排查方法模型选错了工具,通常是因为工具描述不够清晰。排查步骤:把模型的选择和正确选择对比,看它被哪个描述误导了。检查工具描述里是否有歧义词汇。比如“查询”和“获取”可能被模型理解为不同操作。在工具描述里加“使用场景”说明。比如“当用户需要XX时使用此工具”。我踩过的一个坑:有两个工具都叫“查询”,一个是查数据库,一个是查API。模型经常混。后来我把它们改名为“query_database”和“query_api”,并在描述里明确写了数据来源,问题就解决了。6.3 状态膨胀导致提示词过长的处理任务执行到十几步后,completed_steps数组会变得很长,提示词可能超过模型上下文限制。处理方法:只保留最近N步的完整信息,更早的步骤只保留摘要。比如最近5步保留完整结果,更早的只保留“步骤描述+是否成功”。把大结果存到外部,状态里只存引用ID。模型需要时再通过工具读取。定期压缩状态。每5步做一次状态压缩,把已完成的步骤合并成摘要。实操心得:我一般设置状态上限为8000个token。超过后自动触发压缩,把最早的步骤合并成一句话摘要。这样既能保留关键信息,又不会撑爆上下文。6.4 错误处理的最佳实践单循环架构下,错误处理完全依赖模型决策。我的经验是:错误信息要具体。不要只返回“执行失败”,要返回“参数region的值'华东区'无效,有效值为'华东'、'华南'、'华北'”。模型看到具体错误,才能做出正确调整。给模型重试机会,但限制次数。我在状态里记录每个工具的重试次数,超过3次就强制模型换策略。区分可恢复错误和不可恢复错误。网络超时可以重试,参数格式错误需要调整,权限不足需要返回给用户。在提示词里明确这些分类,帮助模型决策。6.5 常见问题速查表问题现象可能原因排查方法解决方案模型反复调用同一工具状态未更新或工具返回空检查状态更新逻辑和工具返回确保状态正确更新,空结果也要写入状态模型选错工具工具描述有歧义对比模型选择和正确选择优化工具描述,加使用场景说明提示词过长状态膨胀检查completed_steps长度压缩状态,只保留最近N步任务提前结束模型误判完成检查finish条件在提示词里明确完成标准错误后不重试错误信息不具体检查工具错误返回返回具体错误信息和有效值循环次数过多任务太复杂或状态不清检查任务步骤数和状态结构拆分任务或优化状态设计7. 一些关于Agent架构的个人体会做Agent这一年,我最大的认知转变是:架构的复杂度应该和任务的不确定性匹配,而不是和功能的丰富度匹配。很多人(包括我自己)一开始会陷入“功能越多越好”的陷阱,觉得加个规划器、加个记忆、加个反思,Agent就更“智能”了。但实际上,每加一个模块,就多一层不确定性,多一层信息损耗,多一层调试成本。单循环架构之所以有效,是因为它把不确定性收敛到了一个地方。模型只需要在一个地方做决策,状态只需要在一个地方维护,错误只需要在一个地方处理。这听起来简单,但真正做到需要克制——克制住“再加一个模块就能解决问题”的冲动。当然,单循环不是终点。当任务复杂度真的超过单循环的处理能力时,还是需要引入分层。但那时候的分层应该是基于明确的数据流和职责边界,而不是基于“这个模块听起来很厉害”。比如,如果任务确实需要并行执行,可以引入一个并行调度层;如果任务确实需要跨会话记忆,可以引入一个持久化层。但这些都应该是按需引入,而不是一开始就堆上去。最后分享一个我常用的判断方法:当你发现Agent效果不好时,先问自己“如果删掉一个模块,会不会更好?”如果答案是“可能会”,那就删掉试试。大多数时候,删减带来的提升比增加更大。这个反直觉的结论,是我这一年做Agent最值钱的经验。
1. 从“堆模块”到“做减法”:一次架构重构的完整复盘做Agent这一年来,我最大的感受就是:这个领域太容易让人产生“加东西就能变强”的幻觉了。每次效果不好,第一反应就是“是不是缺个记忆模块”“是不是该加个规划器”“要不要再挂一个工具路由层”。结果就是架构图越画越复杂,节点越连越多,但实际跑起来的任务完成率却卡在一个瓶颈上死活上不去。直到有一次,我狠下心把整个多层级架构删到只剩一个核心循环,效果反而出现了肉眼可见的提升。这不是玄学,也不是“少即是多”这种正确的废话,而是有具体的工程逻辑在里面的。这篇文章就把这次改动的来龙去脉拆开讲清楚,包括当时踩过的坑、删减的判断依据、删完之后怎么保证功能不丢,以及最终实测下来的效果对比。如果你也在做Agent相关的东西,不管是用现成框架还是自己手搓,这篇内容应该能帮你少走一些弯路。先交代一下背景。我做的Agent主要面向多步骤任务自动化场景,比如根据一段自然语言描述去操作一组内部工具、查询数据、生成结构化报告。这类任务的特点是步骤之间有依赖关系,中间状态需要传递,而且经常需要根据上一步的结果决定下一步做什么。一开始我的思路很“教科书”:既然任务复杂,那就把架构分层——顶层做任务分解,中间层做工具调度,底层做具体执行,再加一个记忆模块存上下文,一个反思模块做结果校验。听起来很合理对吧?但实际跑起来,问题一个接一个。最典型的问题是错误传播。顶层分解出来的子任务,到了中间层可能被重新解释,到了底层执行时又变了一次意思。三层下来,原始意图已经面目全非。另一个问题是延迟叠加。每个模块都要调一次模型,串行下来一个任务跑几十秒甚至几分钟,用户体验极差。还有一个隐性成本:调试困难。当最终结果不对时,你很难定位到底是哪一层的锅,因为每一层都在做“智能决策”,每一层都可能出错。所以当我决定“把架构删了”的时候,其实是在做一个很朴素的判断:与其让多个模块互相猜意图,不如让一个模块从头管到尾。这个判断背后有理论支撑,也有工程上的权衡,下面慢慢展开。2. 为什么“加模块”反而让Agent变笨了2.1 多层级架构的隐性成本:信息损耗与意图漂移很多人做Agent的第一反应是模仿软件工程里的分层架构,觉得这样“解耦”“可维护”。但Agent和传统软件有一个本质区别:传统软件的模块之间传递的是确定的数据结构,而Agent模块之间传递的是自然语言。自然语言本身就是有损压缩,每经过一层“理解-重新表达”,信息就会丢失一部分。我举个实际例子。用户输入是:“帮我查一下上个月华东区销售额超过50万的客户,把他们的联系方式整理成表格,然后给每个客户发一封问候邮件。”这是一个典型的复合任务。在我的旧架构里,顶层规划器会把它拆成三个子任务:查询、整理、发邮件。但顶层规划器在拆解时,可能会把“华东区”这个条件漏掉,或者把“超过50万”理解成“排名前50”。然后中间层拿到这个已经失真的子任务,再去做工具选择,又可能选错工具。底层执行时,即使工具选对了,参数也可能传错。整个过程就像传话游戏,传到最后一个人,原话已经变了。更糟糕的是,每一层都在“自信地”做决策,没有任何一层会主动说“我不确定”。所以最终结果错了,你甚至不知道从哪一层开始错的。这里的关键认知是:Agent的每一层“智能”都是概率性的,概率性的叠加不是线性增长,而是误差累积。层数越多,最终输出的方差越大。2.2 延迟与成本:每个模块都是一次模型调用除了信息损耗,多层级架构还有一个硬伤:延迟。每多一个模块,就多一次模型调用。如果每个模块平均耗时2秒,五个模块串行就是10秒。这还没算工具调用的时间。对于交互式场景,超过5秒用户就开始不耐烦了;对于批量任务,延迟直接转化为成本。我实测过旧架构在一个典型任务上的耗时分布:顶层规划1.8秒,中间调度2.1秒,工具执行平均3.5秒,反思校验2.3秒,记忆更新0.9秒。总计约10.6秒。而删减后的单循环架构,同样的任务平均耗时4.2秒。性能提升了一倍多,而且任务完成率还更高了。成本方面更直观。旧架构每次任务平均消耗约4500个token(因为每个模块都要把上下文重新喂一遍),新架构约1800个token。按大规模调用来算,这是数倍的差距。2.3 调试噩梦:当每一层都在“智能决策”还有一个很少被提及但极其致命的问题:可观测性。在多层级架构里,每一层都在做“智能决策”,这意味着每一层的行为都是不确定的。你没法像调试传统代码那样打断点、看变量。你只能看日志,但日志里全是自然语言,而且每一层的自然语言表达风格还不一样。我印象最深的一次排查:一个任务连续失败了五次,每次失败的原因都不一样。第一次是规划器漏了一个条件,第二次是调度器选错了工具,第三次是执行器参数格式不对,第四次是反思模块误判了结果,第五次是记忆模块把错误信息覆盖了正确信息。五个模块,每个都有“犯错”的能力,组合起来就是灾难。所以当我决定删架构的时候,核心动机就是:把不确定性收敛到一个地方。让一个模块负责从理解到执行的全过程,这样出错时我只需要看一个地方的日志,调整一个地方的提示词,优化一个地方的逻辑。3. 删减策略:保留什么、砍掉什么、合并什么3.1 核心保留项:一个循环 + 一个工具集 + 一份状态删架构不是把东西全扔了,而是重新划分职责边界。我最终保留的东西只有三样:一个主循环:负责接收输入、决定下一步动作、执行动作、判断是否结束。这个循环本质上就是一个“思考-行动-观察”的循环,类似ReAct模式,但做了大量简化。一个工具集:所有可调用的工具都注册在一个扁平的工具列表里,没有分类、没有路由层。工具的描述信息直接写在提示词里,让模型自己选。一份状态:用一个结构化的JSON对象保存任务执行过程中的所有中间状态,包括已完成的步骤、当前步骤、待办事项、关键变量等。这份状态在每个循环迭代时都会被更新,并且完整地传给模型。就这三样。没有规划器、没有调度器、没有反思模块、没有独立的记忆模块。所有这些东西的功能,都被合并到了主循环的提示词和状态管理里。3.2 砍掉规划器:让模型在循环中“边做边想”规划器是我第一个砍掉的模块。原因很简单:在任务开始时就制定完整计划,本质上是在信息不完整的情况下做决策。用户的需求可能模糊,工具的执行结果可能出乎意料,环境状态可能变化。一个在第一步就定死的计划,到了第五步很可能已经完全不适用了。砍掉规划器之后,我改成让模型在每一轮循环中只决定“下一步做什么”。这听起来像是退回到了短视的贪心策略,但实际效果反而更好。因为每一轮循环时,模型都能看到最新的状态(包括上一步的执行结果),所以它的决策是基于最新信息的。这就像开车时看着前方调整方向,而不是出发前就把方向盘焊死。当然,这要求状态设计得非常清晰。模型需要知道:目标是什么、已经做了什么、当前处于什么阶段、有哪些工具可用。这些信息都在提示词里明确给出,模型就能做出合理的下一步决策。3.3 砍掉独立记忆模块:状态即记忆独立的记忆模块也是我早期架构里的一个“标配”。它的职责是存储历史交互、检索相关记忆、更新记忆库。但实际用下来,我发现对于大多数任务型Agent来说,记忆根本不需要独立的模块。为什么?因为任务型Agent的“记忆”本质上就是当前任务的执行状态。用户让你查数据、整理表格、发邮件,这个过程中的所有信息都是围绕当前任务的。你不需要从三个月前的对话里检索什么,你只需要记住这个任务已经执行到哪一步了。所以我把记忆模块砍了,改成一份结构化的状态对象。这份状态在每个循环迭代时都会被完整地传给模型,模型自己决定哪些信息重要、哪些可以忽略。这比让一个独立的记忆模块去“智能检索”要可靠得多,因为检索本身就可能漏掉关键信息。3.4 合并反思与执行:在循环中自然校验反思模块是我最后砍掉的。它的初衷是好的:在任务完成后检查结果是否正确。但问题是,反思模块和主循环看到的信息是一样的,它并没有额外的信息优势。如果主循环都判断不了结果对不对,反思模块凭什么能判断?所以我改成在循环内部做校验。具体来说,每次工具执行后,模型会看到执行结果,然后判断:这个结果是否符合预期?是否需要重试?是否需要调整参数?如果一切正常,就继续下一步;如果异常,就进入错误处理分支。这本质上就是把反思融入了循环,而不是把它作为一个独立的阶段。这样做的好处是校验是即时的。传统架构里,反思是在任务结束后才做的,这时候错误已经传播了很久。而在循环内校验,错误可以在发生的当下就被发现和处理。4. 单循环架构的具体实现与关键细节4.1 主循环的伪代码与状态结构设计先看主循环的核心逻辑。我用的是Python风格的伪代码,实际实现可以是任何语言:def run_agent(user_input, tools, max_iterations=20): state = { "goal": user_input, "completed_steps": [], "current_step": None, "pending_steps": [], "variables": {}, "last_error": None, "iteration": 0 } while state["iteration"] < max_iterations: state["iteration"] += 1 # 构建提示词,包含目标、状态、工具描述 prompt = build_prompt(state, tools) # 模型决策:返回下一步动作 action = model.decide(prompt) # 判断是否结束 if action.type == "finish": return action.result # 执行工具 try: result = execute_tool(action.tool_name, action.parameters) state["completed_steps"].append({ "step": action.description, "tool": action.tool_name, "result": result }) state["last_error"] = None except Exception as e: state["last_error"] = str(e) # 不中断循环,让模型决定如何处理错误 # 更新变量 if action.updates: state["variables"].update(action.updates) return {"error": "达到最大迭代次数"}这个循环看起来很简单,但有几个关键设计点:第一,状态是显式传递的。每一轮循环都把完整状态传给模型,模型不需要“记住”什么,所有信息都在提示词里。这消除了对记忆模块的依赖。第二,错误不中断循环。工具执行失败时,错误信息被写入状态,然后继续下一轮循环。模型会看到错误信息,并决定是重试、换工具、还是调整参数。这比直接抛异常要健壮得多。第三,有最大迭代次数限制。这是防止无限循环的安全阀。实际使用中,大多数任务在5到10轮内完成,20轮足够覆盖复杂任务。4.2 提示词工程:如何让模型在循环中不迷路单循环架构对提示词的要求比多模块架构更高,因为所有决策都集中在一个地方。我的提示词模板经过多次迭代,最终稳定在以下结构:你是一个任务执行Agent。你的目标是用最少的步骤完成用户的任务。 ## 当前目标 {goal} ## 当前状态 - 已完成步骤:{completed_steps} - 当前变量:{variables} - 上一步错误:{last_error} ## 可用工具 {tool_descriptions} ## 决策规则 1. 每次只决定下一步做什么,不要规划多步。 2. 如果上一步有错误,优先处理错误。 3. 如果任务已完成,返回 finish。 4. 如果需要调用工具,明确指定工具名和参数。 5. 如果信息不足,调用工具获取信息,不要猜测。 ## 输出格式 {output_format}这个提示词有几个关键点:明确要求“每次只决定下一步”。这防止模型在单次决策中试图规划多步,导致输出过长且容易出错。错误处理优先级最高。如果上一步出错,模型会优先处理错误,而不是继续往下走。强调“不要猜测”。这是减少幻觉的关键。模型在信息不足时应该调用工具,而不是编造答案。输出格式严格约束。我用JSON schema约束输出,确保模型返回的是结构化的动作,而不是自由文本。实操心得:提示词里的“决策规则”部分不要写太多条,超过7条模型就开始忽略后面的。我试过写15条规则,结果模型只遵守前5条。后来精简到5条,遵守率明显提升。4.3 工具设计:扁平化注册与描述优化工具集的设计也经历了大改。旧架构里,工具是分类注册的,有“查询类”“操作类”“计算类”等。模型需要先选类别,再选具体工具。这多了一步决策,而且类别划分本身就可能模糊。新架构里,所有工具扁平注册,模型直接从列表里选。工具描述我做了大量优化,核心原则是:让模型一眼看懂这个工具是干什么的、什么时候该用、参数怎么传。一个典型的工具描述长这样:{ "name": "query_sales_data", "description": "查询销售数据。当用户需要获取销售额、客户信息、订单记录时使用此工具。", "parameters": { "region": "地区名称,如'华东'、'华南'。不传则查询全部地区。", "min_amount": "最小金额筛选,单位元。不传则不筛选。", "time_range": "时间范围,格式'YYYY-MM'。不传则查询当月。" }, "returns": "返回符合条件的记录列表,每条包含客户名、联系方式、销售额、日期。" }注意几个细节:description里明确写了“什么时候用”。这帮助模型在正确的场景下选择正确的工具。参数说明里写了“不传则...”。这减少了模型因为不确定参数而卡住的情况。returns里描述了返回结构。这让模型能预判执行结果,从而更好地决定下一步。我实测下来,工具描述的质量对任务完成率的影响非常大。同样的工具集,描述优化前后,任务完成率差了将近20个百分点。4.4 状态管理:用JSON Schema约束中间数据状态管理是单循环架构的基石。如果状态设计得不好,模型在循环中就会“迷路”。我的状态对象用JSON Schema严格约束,确保每个字段都有明确的类型和含义。{ "goal": "string, 用户原始输入", "completed_steps": [ { "step": "string, 步骤描述", "tool": "string, 使用的工具名", "result": "object, 工具返回结果", "timestamp": "string, ISO时间" } ], "variables": { "key": "value" }, "last_error": "string or null", "iteration": "integer" }关键设计点:completed_steps是数组,保留完整历史。模型可以看到每一步做了什么、结果是什么。这比只保留“当前状态”要可靠,因为模型可以回溯。variables是自由字典。用于存储任务执行过程中提取的关键信息,比如“客户列表”“筛选条件”等。模型可以往里面写,也可以从里面读。last_error单独字段。让模型一眼看到上一步是否出错,优先处理。注意事项:状态对象不要太大。我试过把完整的工具返回结果都塞进状态,结果提示词长度爆炸,模型反而抓不住重点。后来改成只保留关键字段,比如查询结果只保留前10条记录和总数,效果反而更好。5. 实测效果对比与典型场景分析5.1 任务完成率与延迟的量化对比我用了三组任务做对比测试,每组50个任务,覆盖简单查询、多步操作、异常处理三种类型。旧架构和新架构的对比结果如下:指标旧多层级架构新单循环架构变化简单查询完成率92%96%+4%多步操作完成率68%84%+16%异常处理完成率45%72%+27%平均延迟(简单)6.2秒2.8秒-55%平均延迟(多步)12.4秒5.1秒-59%平均token消耗42001750-58%多步操作和异常处理的提升最明显。这符合预期,因为这两类任务最容易受到信息损耗和错误传播的影响。单循环架构下,模型始终看到完整状态,决策基于最新信息,所以表现更稳定。5.2 一个多步任务的完整执行记录拿一个实际任务举例:“查一下上个月华东区销售额超过50万的客户,整理成表格,然后给每个客户发一封问候邮件。”旧架构的执行过程:规划器拆解:查询客户 → 整理表格 → 发邮件(漏掉了“华东区”和“50万”条件)调度器选择工具:query_customers(参数只有时间范围)执行器返回:所有客户列表(几百条)反思模块:结果太多,可能不对,但无法判断哪里不对最终输出:一个包含几百条记录的表格,任务失败新架构的执行过程:循环1:模型看到目标,决定调用query_sales_data,参数region=华东, min_amount=500000, time_range=上个月循环2:工具返回23条记录。模型判断需要整理成表格,调用format_table工具循环3:表格生成成功。模型判断需要发邮件,调用send_email工具,参数为23个客户的邮箱和问候模板循环4:邮件发送成功。模型返回finish整个过程4轮循环,耗时约4.5秒,任务完成。关键区别在于:模型在每一步都能看到完整的目标和最新的状态,所以不会漏条件、不会选错工具、不会误判结果。5.3 什么情况下单循环架构会吃力单循环架构不是万能的。我实测下来,它在以下场景会吃力:超长任务(超过20步):状态对象会变得很大,提示词长度爆炸,模型注意力分散。这种场景可能需要引入分层,但分层的方式要重新设计。需要并行执行的任务:单循环是串行的,如果任务中有多个独立子任务可以并行,单循环会浪费时间。不过大多数任务型Agent的场景都是串行的,所以这个问题不突出。需要长期记忆的任务:如果任务需要跨会话记住用户偏好,单循环架构需要额外设计持久化机制。但这和架构层数无关,是另一个维度的问题。所以我的建议是:先从单循环开始,遇到瓶颈再考虑加东西。不要一上来就搞复杂架构,那样你连问题出在哪都找不到。6. 常见问题与排查技巧实录6.1 模型在循环中“卡住”怎么办这是最常见的问题。模型反复调用同一个工具,或者反复返回相同的错误。排查思路:检查状态更新逻辑:是不是工具执行后状态没有正确更新?模型看到的状态和实际状态不一致,就会重复决策。检查工具返回:是不是工具返回了空结果或异常结果,但状态里没有体现?模型不知道失败了,就会一直重试。检查提示词:是不是没有明确告诉模型“如果连续两次相同操作,必须换策略”?我在提示词里加了一条规则:“如果上一步和当前步骤相同,必须改变策略或返回错误。”6.2 工具选择错误的排查方法模型选错了工具,通常是因为工具描述不够清晰。排查步骤:把模型的选择和正确选择对比,看它被哪个描述误导了。检查工具描述里是否有歧义词汇。比如“查询”和“获取”可能被模型理解为不同操作。在工具描述里加“使用场景”说明。比如“当用户需要XX时使用此工具”。我踩过的一个坑:有两个工具都叫“查询”,一个是查数据库,一个是查API。模型经常混。后来我把它们改名为“query_database”和“query_api”,并在描述里明确写了数据来源,问题就解决了。6.3 状态膨胀导致提示词过长的处理任务执行到十几步后,completed_steps数组会变得很长,提示词可能超过模型上下文限制。处理方法:只保留最近N步的完整信息,更早的步骤只保留摘要。比如最近5步保留完整结果,更早的只保留“步骤描述+是否成功”。把大结果存到外部,状态里只存引用ID。模型需要时再通过工具读取。定期压缩状态。每5步做一次状态压缩,把已完成的步骤合并成摘要。实操心得:我一般设置状态上限为8000个token。超过后自动触发压缩,把最早的步骤合并成一句话摘要。这样既能保留关键信息,又不会撑爆上下文。6.4 错误处理的最佳实践单循环架构下,错误处理完全依赖模型决策。我的经验是:错误信息要具体。不要只返回“执行失败”,要返回“参数region的值'华东区'无效,有效值为'华东'、'华南'、'华北'”。模型看到具体错误,才能做出正确调整。给模型重试机会,但限制次数。我在状态里记录每个工具的重试次数,超过3次就强制模型换策略。区分可恢复错误和不可恢复错误。网络超时可以重试,参数格式错误需要调整,权限不足需要返回给用户。在提示词里明确这些分类,帮助模型决策。6.5 常见问题速查表问题现象可能原因排查方法解决方案模型反复调用同一工具状态未更新或工具返回空检查状态更新逻辑和工具返回确保状态正确更新,空结果也要写入状态模型选错工具工具描述有歧义对比模型选择和正确选择优化工具描述,加使用场景说明提示词过长状态膨胀检查completed_steps长度压缩状态,只保留最近N步任务提前结束模型误判完成检查finish条件在提示词里明确完成标准错误后不重试错误信息不具体检查工具错误返回返回具体错误信息和有效值循环次数过多任务太复杂或状态不清检查任务步骤数和状态结构拆分任务或优化状态设计7. 一些关于Agent架构的个人体会做Agent这一年,我最大的认知转变是:架构的复杂度应该和任务的不确定性匹配,而不是和功能的丰富度匹配。很多人(包括我自己)一开始会陷入“功能越多越好”的陷阱,觉得加个规划器、加个记忆、加个反思,Agent就更“智能”了。但实际上,每加一个模块,就多一层不确定性,多一层信息损耗,多一层调试成本。单循环架构之所以有效,是因为它把不确定性收敛到了一个地方。模型只需要在一个地方做决策,状态只需要在一个地方维护,错误只需要在一个地方处理。这听起来简单,但真正做到需要克制——克制住“再加一个模块就能解决问题”的冲动。当然,单循环不是终点。当任务复杂度真的超过单循环的处理能力时,还是需要引入分层。但那时候的分层应该是基于明确的数据流和职责边界,而不是基于“这个模块听起来很厉害”。比如,如果任务确实需要并行执行,可以引入一个并行调度层;如果任务确实需要跨会话记忆,可以引入一个持久化层。但这些都应该是按需引入,而不是一开始就堆上去。最后分享一个我常用的判断方法:当你发现Agent效果不好时,先问自己“如果删掉一个模块,会不会更好?”如果答案是“可能会”,那就删掉试试。大多数时候,删减带来的提升比增加更大。这个反直觉的结论,是我这一年做Agent最值钱的经验。