如果你想用大模型做点能真正落地的东西,Agent 大概是绕不开的一个话题。这两年大模型本身的能力突破大家有目共睹,但纯靠模型单轮问答能解决的需求太有限了。真正让大模型发挥价值的方式,是让它去拆任务、调工具、看结果、再迭代——也就是把它包装成一个 Agent 系统。而 Agent 系统开发里最容易拉开差距、也最容易翻车的地方,恰恰是工作流设计。
这篇东西是我自己从几个 Agent 项目里反复重构、踩坑之后沉淀下来的实战总结。我会把工作流设计的核心思路、三种常用模式、工程化落地时的常见卡点,以及排查问题的实际经验一次性讲清楚。内容上更侧重“怎么把流程写稳”,而不是堆模型参数。适合两类人看:一类是刚接触 Agent 开发、想避开低级坑的工程师;另一类是已经写了几个 Demo、但一上生产环境就被各种异常搞崩的开发者。
1. 先别急着写代码:把 Agent 的工作流想清楚
1.1 所谓 Agent,核心不是模型,而是流程控制
很多初学者会把 Agent 理解成“接一个大模型 API,然后连续对话”。这个理解不能说错,但远远不够。真正意义上的 Agent 系统,至少包含四部分:模型、工具、记忆、流程控制。其中流程控制是最容易被忽略、却又决定系统上限的部分。
我习惯用一个类比来解释:纯大模型调用就像你雇了一个知识渊博的实习生,他懂得多,但你给他布置任务,他只会一次性回答你。而 Agent 系统,是给这个实习生配了项目经理、工作手册、工具箱和复盘机制——项目经理负责拆解任务,工作手册告诉他哪一步该做什么,工具箱能查数据、能发请求、能写文件,复盘机制确保他做完一轮之后回看结果对不对。
这里的“项目经理”和“复盘机制”,本质上就是工作流。工作流设计的好坏,直接决定了 Agent 是“一个能连续对话的聊天机器人”,还是“一个能独立完成复杂任务的自动化系统”。
另一个常见的误区是:一味追求模型的推理能力,把全部希望押在“强模型自己会想清楚”。模型确实越来越聪明,但聪明不代表稳定。实测下来,哪怕是很强的商用模型,在完全自由发挥的情况下,也容易漏掉关键步骤、忘记中间结果、或者在一个错误分支上反复打转。工作流的作用,就是把这些不确定性收束到可控的轨道里——给模型划出跑道,它才能跑得快而不跑偏。
1.2 工作流设计的三件顶层事:任务边界、确定性与失败收口
在设计一个 Agent 系统的工作流之前,我会先逼自己回答三个问题,回答清楚了才动代码。
第一个问题是任务边界。这个 Agent 到底负责什么,不负责什么?边界划得太宽,Agent 会被各种无关输入带跑;划得太窄,系统又失去了灵活性。我做过一个客服工单分类的 Agent,最初的设计是“任何问题都能处理”,结果模型经常跑去回答天气、算数学题,把工单系统搞得一团糟。后来把所有子任务收敛到“工单分类、紧急度判断、知识库检索、答案生成”四个环节,效果立刻稳了。这不是模型变强了,而是流程边界清楚了。
第二个问题是确定性。哪些步骤必须由代码控制,哪些步骤可以交给模型自由发挥?我的原则是:能枚举的走规则,不能枚举的才交给模型。比如判断一个输入是工单还是闲聊,完全可以用轻量规则先过滤;而判断工单的意图归类,则需要模型理解语义。把确定性的部分交给代码,把不确定性的部分交给模型,这一条我后来在几乎所有项目里反复验证,都能显著降低错误率。
第三个问题是失败收口。Agent 一定会出错,这不是概率问题,而是时间问题。关键不是避免错误,而是错误发生之后,工作流能走到哪个收口逻辑。是重试一次?换一个工具?降级成人工处理?还是直接返回“我不确定”?我见过太多 Agent 系统死在“模型遇到异常后自作主张编了一个答案”这种场景里。工作流设计里必须有明确的失败分支和终止条件,核心原则是:宁可告诉用户“处理不了”,也不让模型瞎编一个看似合理的错误结论。
2. Agent 工作流的三种基础模式与拆解
2.1 线性路由式工作流:最稳妥的起点
线性路由式工作流是所有 Agent 架构里最基础、也最不容易出错的一种。它的核心思想是:输入先经过一个“路由器”判断类型,然后分发给对应的子处理流程,每个子流程内部是固定的步骤序列。
举一个我实际做过的例子:某内容平台的评论审核 Agent。整个工作流包括四个环节——先识别评论类型(询问型、抱怨型、灌水型、举报型),然后对不同类型的评论走不同处理链。询问型走知识库检索,抱怨型走安抚话术生成,举报型直接转人工工单,灌水型直接过滤。这个系统上线后效果非常稳定,核心原因就是每个分支的步骤都是预先定义好的,模型只需要在关键节点做判断,不需要自由发挥整个流程。
线性路由式的优点是清晰、可控、易排查。出问题了能准确定位到是哪一个环节出了错,日志里拿到的信息非常结构化。缺点是灵活性有限,处理不了需要多轮迭代的复杂任务。但它特别适合作为第一个 Agent 项目的起点——先跑通基本的“分类-处理”链路,后面再往里面加循环、加分支。
2.2 循环反思式工作流:用迭代换质量
循环反思式工作流解决的是一个很实际的痛点:模型一次生成的结果往往不够好。不管是写代码、写文案还是做分析报告,第一版总会有这样那样的问题。与其指望模型“一次性完美”,不如在流程里加入自检和修正环节。
这类工作流的基本结构是:先生成一个草稿,然后让模型(或者独立的规则检查器)对草稿进行评估,评估不通过就打回修改,通过则输出。我在一个自动生成周报的项目里用过这个模式:第一轮生成周报初稿,第二轮让模型对照“是否遗漏数据、是否夸大成果、格式是否符合要求”三个维度自评,第三轮根据自评结果修订。实测下来,经过一轮反思修订的报告,在可读性指标上比初稿提升了 30% 以上。
不过这个模式有个必须警惕的问题:迭代次数失控。模型有时候会在同样的修改意见里反复横跳,甚至越改越差。所以循环反思式工作流必须有硬性上限。我通常的做法是设置“最大反思轮次”(一般 2 到 3 轮),到达上限后强制输出当前版本,避免无限循环烧掉大量 token。另外,反思的维度要写得具体——与其让模型“检查一下有没有问题”,不如让它“逐条检查:结论是否有数据支撑,格式是否符合约定,是否包含行动项”,这样修正的效果会好很多。
2.3 主从任务分解式工作流:复杂任务的正解
当一个任务复杂到单次推理无法完成时,主从任务分解式工作流就派上用场了。它的原理是:由主 Agent 担任“项目经理”,把大任务拆成多个子任务,分别分配给子 Agent 或工具执行,最后再由主 Agent 汇总结果。
这种模式非常适合“调研-分析-产出”这个链条。比如我做过一个行业竞品分析 Agent:主 Agent 先拆出“产品功能调研”“价格策略分析”“用户评价摘要”“SWOT 总结”四个子任务;前三个任务各自调用搜索工具和数据接口去搜集信息;信息汇总回来后,主 Agent 统一做整合分析,输出最终报告。
主从式工作流的关键实践细节有两个。一是子任务的结果格式必须统一。无论子任务产出的是什么类型的内容,返回给主 Agent 时都应该是结构化文本,带明确的字段名,否则主 Agent 整合时会花费大量 token 去“理解”混乱的中间结果。二是子任务之间尽量保持独立,不要设计成 A 子任务的结果依赖 B 子任务的中间状态,一旦有依赖,流程的维护成本和失败率都会急剧上升。如果确实有依赖,就要考虑把下游子任务设计成等待外部条件触发的挂起状态,而不是在同一个循环里硬串。
2.4 三种模式的选型逻辑
做了一个表格,方便快速对照选型。
| 工作流模式 | 核心结构 | 适用场景 | 主要风险 | 推荐度 |
|---|---|---|---|---|
| 线性路由式 | 前置分类 + 固定步骤链 | 意图分发、工单处理、内容分类 | 分支判断错误,流程过于刚性 | 新手首选 |
| 循环反思式 | 生成 → 评估 → 修订 | 内容写作、代码生成、报告初稿 | 迭代失控,成本膨胀 | 中等复杂度项目常用 |
| 主从分解式 | 拆任务 + 并行执行 + 汇总 | 调研分析、方案规划、复合业务 | 子任务依赖混乱,汇总困难 | 复杂任务首选 |
选型的判断逻辑并不复杂:如果任务能在两步以内解决,线性路由就够了;如果结果质量不达标是主要矛盾,加反思循环;如果任务本身大而复杂、必须拆解,再上主从分解。不要一上来就设计一个融合所有模式的超复杂架构,Agent 系统的复杂度每上一个台阶,排错成本是成倍增长的。
3. 实操:搭建一个最小可用的 Agent 工作流骨架
3.1 模块划分与核心数据结构
理论说得再多,不如直接看代码。我下面给出的是一个经过生产验证的最小骨架,基于 Python 实现,结构上分成五个模块:模型调用层、任务编排层、工具层、记忆层、观测层。这个骨架的目的是让你理解工作流的核心循环长什么样,实际项目可以直接在这个基础上扩展。
模块设计的核心原则是:每一层的职责必须单一。模型调用层只管发请求、拿响应、统计 token;任务编排层只管流程控制,决定下一步是调工具还是生成输出;工具层统一封装外部能力,暴露统一接口给编排层调用;记忆层管理长期存储和上下文拼接;观测层负责记录完整的运行轨迹。
from enum import Enum from dataclasses import dataclass, field from typing import Any, Optional class WorkflowState(Enum): PENDING = "pending" PLANNING = "planning" EXECUTING = "executing" TOOL_CALL = "tool_call" REFLECTING = "reflecting" FINISHED = "finished" FAILED = "failed" @dataclass class TaskMessage: task_id: str input: str state: WorkflowState = WorkflowState.PENDING plan: list[str] = field(default_factory=list) history: list[dict] = field(default_factory=list) tool_results: dict[str, Any] = field(default_factory=dict) final_output: Optional[str] = None error_info: Optional[str] = None attempts: int = 0TaskMessage 是整个工作流运行时的核心数据结构。task_id 用于全链路追踪;plan 存放拆解出来的子任务列表;history 记录和模型之间的完整交互;tool_results 保存每个工具调用的返回结果,供后续步骤引用;attempts 记录当前任务的重试次数,配合上限使用。
这里有一个很多人忽视的细节:状态枚举必须显式设计,而不是靠模型判断“我现在该干什么”。在编排层里,每一步的下一步是代码决定的,模型只负责生成内容、调用工具、或者根据结果做判断,它永远不负责决定“流程该往哪里走”。把流程控制权从模型手中收走,是 Agent 系统稳定运行的第一前提。
3.2 工具注册与调用协议
工具层是 Agent 工作流里最容易出问题的环节。模型要正确调用一个工具,必须知道这个工具是干什么的、参数怎么传、返回什么。这三件事,全部要靠在注册阶段写清楚。
我维护工具注册表的经验是:工具描述要写“业务意图”,而不是“函数签名”。比如一个查询数据库的工具,不要写“execute_query(sql)”,而要写“根据用户问题生成 SQL 并查询业务数据库,返回查询结果,仅供内部数据使用,不处理身份认证类问题”。模型看到这种描述,才真正知道什么时候该用它、什么时候不该用它。
@dataclass class ToolSpec: name: str description: str parameters: dict handler: callable class ToolRegistry: def __init__(self): self._tools: dict[str, ToolSpec] = {} def register(self, spec: ToolSpec): self._tools[spec.name] = spec def call_tool(self, name: str, arguments: dict) -> dict: spec = self._tools.get(name) if spec is None: return {"success": False, "error": f"tool {name} not found"} try: result = spec.handler(**arguments) return {"success": True, "result": result} except Exception as e: return {"success": False, "error": str(e)}工具调用的返回必须统一包装成 success / error / result 的结构。这个设计是踩坑踩出来的教训。早期我在一个 Agent 里让工具直接返回原始字符串,模型经常把错误信息误当成正常答案,编造出一些不存在的结论。统一成结构化返回之后,编排层可以明确判断:success 为 false 时,走重试或者失败分支,模型没有机会对错误结果“自由发挥”。
另外一个很重要的经验是:给工具设计参数时,尽量用简单类型,避免让模型生成复杂的嵌套对象。模型生成一个扁平的关键字参数远比生成一个深层嵌套的 JSON 稳定得多。实测中嵌套参数的出错率是扁平参数的 3 倍以上。
3.3 状态持久化与上下文管理
Agent 跑起来之后,一定会遇到两个实际问题:进程重启了怎么恢复现场,以及上下文窗口装不下所有历史记录怎么办。
先看状态持久化。我给每个 TaskMessage 设置一个序列化方法,每隔几个关键步骤就把它保存到 Redis 或者数据库里。这样即使进程崩溃、服务重启,也能从最新的检查点恢复,而不是整个任务推倒重来。检查点保存的频率不需要太高——每完成一个子任务保存一次就够了,太频繁会拖慢性能。
再看上下文管理。大模型的上下文窗口是有限的,而 Agent 系统的对话历史会持续膨胀,尤其是反复调用工具、反思纠错之后,几张表格就塞满了窗口。我的做法是“分层记忆”:把最近的 2 到 3 轮完整对话保留在上下文里,更早的历史用摘要压缩成一段文字,最久远的信息则落库到向量库,按需检索回来补进上下文。
def build_context(history: list[dict], max_history: int = 3) -> str: recent = history[-max_history:] older = history[:-max_history] summary = summarize_old_history(older) if len(older) > 3 else "" parts = [] if summary: parts.append(f"[早期上下文摘要]\n{summary}") for item in recent: parts.append(item["content"]) return "\n\n".join(parts)上下文压缩会损失部分细节,所以这里需要做一个权衡。我的经验是:摘要只负责保留“已经确定的结论、已完成的操作、关键数据点”,不保留过程性细节。比如调研任务里,摘要里写“已完成竞品 A 的功能调研,共提取 5 个核心功能点,其中可视化仪表盘为重要发现”就足够,而不需要完整记录模型是怎么一步步查到的。这样压缩后的上下文,既能支撑后续决策,又不会把窗口塞爆。
4. 工程化落地:卡点与解法
4.1 给工作流装上“刹车”:轮次上限、超时与熔断
Agent 工作流跑在真实服务里,最可怕的不是结果不好,而是任务悬在那里不结束,还持续消耗计算资源。我在上线第一个 Agent 项目的第一周就遇到过一次:一个任务因为模型始终认为“需要再检查一次”,循环了将近 30 分钟,费用账单感人。从此之后,“刹车机制”成了我所有 Agent 项目的标配。
第一层是轮次上限。每个任务在创建时就带上 max_iterations 参数,我用一个简单的公式估算:预估 token 消耗上限 ÷ 每轮平均 token 消耗 × 安全系数。比如一个项目总预算 1000 万 token,每轮平均消耗 2 万 token,理论可以跑 500 轮,但我只设 50 轮,因为正常情况下一个好的任务绝对不需要 500 轮,超过 50 轮基本说明流程出了问题,还不如趁早失败。
第二层是超时控制。这里的超时不是指模型 API 的单次超时,而是整个工作流的执行超时。我给每个任务设置了 wall-clock 时限,比如 10 分钟。超时后不管任务跑到哪个环节,统一进入收尾流程,返回当前已获得的最佳结果,或者明确告知用户“任务超时,请重试”。
第三层是熔断。当一个任务在短时间内连续失败(比如工具调用失败超过 3 次),工作流应该立即停止重试,转入降级分支。降级分支可以是让用户复议、转录人工处理,或者返回一个安全的默认答案。不要相信模型在连续失败后会“灵光一现”找到新路径,在真实环境里,连续失败通常意味着系统性的问题,不是多试一次能解决的。
4.2 并发与资源控制:队列、限流、缓存
Agent 系统上线之后,并发是躲不掉的。一个问答机器人、一个自动报告生成器,只要用户量上来,同一时间可能有几十个 Agent 实例在跑。这些实例共享模型 API 配额和工具资源,如果不做控制,一个小小的流量尖峰就能把所有任务都拖垮。
我的做法是引入任务队列。所有 Agent 任务进来后先进入队列,由调度器按照优先级和资源配额分发给执行器。执行器数量根据模型 API 的并发限制来配置。假设 API 的并发上限是 20,那么执行器就设 15 个,留出缓冲余量。队列里每个任务的等待时长要监控——如果等待超过 30 秒,说明资源不够,需要扩容或者限流。
限流方面,我对同一用户的任务速率做限制,比如每个用户同时最多只能有 2 个任务在执行。这个限制不是技术限制,而是业务保护——避免个别用户的大量请求占满所有资源。
缓存是另一个常被忽略的成本优化点。对于内容相似度极高的任务,比如同一批数据的汇总报告,完全可以用任务的输入哈希作为缓存 key,命中直接返回历史结果。我在一个周报生成 Agent 里做了缓存之后,同一周的重复请求中有 35% 直接命中缓存,成本和时延同时降了下去。
4.3 可观测与评测:日志、追踪与回归测试
Agent 系统比传统服务的排错难度高得多,因为传统服务的错误是确定性的,而 Agent 系统的错误往往是概率性的——同样的输入,这次能成功,下次可能就失败。想让这种系统变得可维护,必须在三个层面做投入。
第一个层面是日志。每一轮交互、每一次工具调用、每一次状态变更,都要记录到结构化日志里。我定义了一套日志规范,每个日志事件至少包含:task_id、状态机从哪个状态转到哪个状态、模型输入输出的 token 数、工具名称和返回码、耗时。这套日志让我可以在出问题时把一次完整的 Agent 运行过程完整回放出来。
第二个层面是追踪。为了把多个模块的日志串成一个链路,我在 TaskMessage 里已经放了 task_id,所有层级的日志都带上这个字段。配合现成的链路追踪工具,可以直观地看到某个任务在哪一步耗时最多、哪一步错误频发。
第三个层面是评测。这是 Agent 系统和传统服务最大的不同点。传统功能用单元测试就能覆盖,Agent 系统的表现没法简单断言,所以我维护了一套“金样评测集”:把日常任务里最有代表性的 20 到 30 个输入收集起来,任何工作流改动后都跑一遍离线评测,人工看看输出质量有没有劣化。这个机制看着简陋,但在实际项目中救了我很多次——它可以在改动上线前就发现问题,而不是等用户反馈才去救火。
评测的时候我一般看三个指标:任务完成率(成功走到最终状态的比例)、工具调用准确率(模型选对工具并传对参数的比例)、以及关键步骤有效率(中间产物是否真正被后续步骤使用)。三个指标一起看,比单纯看最终输出质量更能定位问题出在哪个环节。
5. 避坑指南:反复出现的问题与排查方法
5.1 问题一:循环失控,任务无限打转
这是 Agent 系统最容易出现的问题,表现是日志里同一个步骤反复执行,模型的反思方向反复横跳,永远拿不出最终答案。我在一个自动写竞品分析报告的项目里遇到过,模型每次回顾都说“信息还不够,需要再搜索一次”,其实再搜回来的内容跟之前的基本重复。
这种问题的根因通常有两个。一是评估条件写得太空泛,模型判断“是否完成”时缺乏明确标准,就会默认“不够好”。应对方式是给评估环节一个显式的完成标准列表,每一项都要能对照检查,不满足才打回。二是没有硬性轮次上限,让模型可以无限次“再来一轮”。应对方式是兜底参数,两条同时做,缺一不可。
排查时我会先看反思轮次分布:如果连续多轮反思都是同一个理由,说明评估标准设计失效;如果轮次飙升但理由各不相同,说明任务本身设计过重,需要重新划分子任务。
5.2 问题二:工具调用异常导致流程卡死
工具调用异常分两种:一种是工具本身报错,包括超时、参数错误、数据源不可用;另一种是工具返回值结构不符合预期,导致后续解析逻辑直接抛异常。前者好判断,后者非常隐蔽。
我印象很深的一个案例是:某个数据库查询工具偶尔返回空列表,正常情况下这不影响流程,但因为后续代码用索引访问第一行,直接抛了 IndexError,整个工作流崩了。从那以后,我在工具返回规范里增加了一条铁律:工具结果里任何列表字段的消费方,都必须先说“如果为空则如何处理”。这个规则听着简单,但在实际项目里一抓一个准。
处理工具异常的通用策略是分级处理:网络类错误(超时、连接失败)自动重试一次再隔几秒重试一次;参数类错误直接把错误信息回传给模型,让它自己修正参数;数据类错误(查不到、结果为空)走“跳过该步骤继续”或者“更换数据来源”的分支,绝不阻塞在错误点上。
5.3 问题三:上下文爆掉、输出质量断崖式下降
大模型的上下文压缩策略不是无限的,当对话历史越来越长,早期的信息被压缩得越来越厉害,Agent 经常会“忘记”最开始的任务目标,或者把某些中间信息错当成最终结论。凡是跑过多轮反思或主从分解任务的人,应该都见过这种症状:越往后,生成的结果离初始需求越远。
我的排查经验是:先看日志里每一轮的摘要信息,确认摘要压缩是否丢失了关键约束条件。比如用户一开始明确提出“报告控制在 500 字以内”,但这个要求在第二轮后就被摘要压缩掉了,模型后续生成的内容自然越来越离谱。解决方案是把这一类“不可丢失约束”单独存储在元数据里,每次构建上下文时强制注入,不经过摘要压缩。换句话说,摘要只管压缩动态内容,静态需求字段独立保存,防止模型遗忘。
另一个派生问题是:输出质量下降不一定是因为上下文爆了,也可能是温度参数没有随流程阶段调整。在反思评估环节,温度设低一点(0 到 0.3),模型会更严谨;在创意生成的环节,温度适当地设高一些(0.5 到 0.7),输出会更有发散性。同一个工作流里不同阶段用不同温度,是我一直保持的习惯。
5.4 常见问题速查表
| 现象 | 直接原因 | 排查手法 | 修复建议 |
|---|---|---|---|
| 任务无限循环 | 评估标准模糊、缺少轮次上限 | 检查反思轮次分布,确认打回理由是否重复 | 显式完成标准列表 + 硬性轮次上限 |
| 工具调用报错但流程继续 | 错误分支缺失或回退逻辑不完整 | 回放日志定位工具返回码 | 补充分级错误处理分支 |
| 结果离题越滚越远 | 关键约束被摘要压缩 | 对比摘要内容和初始需求字段 | 关键约束元数据独立存储,强制注入 |
| 并发一高就大面积超时 | 执行器数量超过 API 配额,任务排队堆积 | 观察队列长度和执行器负载 | 队列 + 限流 + 执行器数量配比 |
| 某一步偶发失败 | 工具返回空值、结构异常 | 检查工具消费代码对空值的处理 | 所有字段消费前明确空值策略 |
| 改动后总体质量下降 | 没有回归测试兜底 | 跑金样评测集对比改动前后输出 | 维护 20 条以上金样用例,改动即评测 |
6. 实战沉淀:我最终保留的五条工作流设计原则
踩了足够多的坑之后,我把所有经验收敛成了五条原则,现在每个新的 Agent 项目我都会拿这五条过一遍设计。
第一条是流程控制权永远在代码手里,模型只做内容生成和局部判断,绝不让模型决定“流程下一步怎么走”。第二条是确定性逻辑走代码,非确定性判断走模型,能枚举的分支就不要让模型去悟。第三条是错误分支必须显式设计,凡是模型可能出错的地方都要提前想好“出错了怎么办”。第四条是长期记忆里的关键约束不能压缩,该独立存储的必须独立存储。第五条是上线前必须有金样评测,否则任何一次改动都是在裸奔。
这五条看着都非常朴素,但我在真实项目里验证过太多次了。很多 Agent 项目之所以 demo 跑得通、上线就崩,不是因为模型不够强,而是因为这五条里的某一条被忽略了。我早期最惨痛的一次教训就是在一版工作流里让模型自己决定“什么时候该结束任务”,结果模型永远觉得自己还没准备好,一个简单任务跑出了三十多轮调用。
如果你正在规划自己的第一个 Agent 项目,我的建议是把做最小可行工作流当成第一步,而不是先上最强的模型、搭最复杂的架构。先用线性路由式跑通一个完整闭环,再加一个反思循环,再加工具调用。每加一层复杂度就回到那五条原则去检查一遍。这个迭代节奏虽然不刺激,但特别稳。
最后再分享一个小技巧:工作流设计完成之后,花半天时间把所有“假设”写下来。你假设模型一定会在某些场景下调用某个工具、假设工具一定会在几秒内返回、假设某个中间结果一定不会为空——这些假设,就是未来最常见的故障点。提前写下来,后面排查问题会省你很多时间。