AI Agent 和工作流怎么选:两个自动化项目走出了完全不同的路子
这半年里我连续碰到过两个 AI 自动化项目,表面上看都很适合做 Agent,最后做出来的路子却完全不同。一个越做越像一条写死的流水线,另一个越做越离不开模型自己拿主意。
第一个项目是内部知识处理流程。输入是文档,输出是摘要、标签、结构化字段,再把结果入库。第二个项目是一个运维助手,目标是接收自然语言指令后去查资料、调用工具、生成处理建议。
最开始团队对这两个项目的直觉差不多:都上 Agent 吧,毕竟多步骤、会调用工具、看起来很智能。
把两个项目并排放着看,第一个项目如果真按 Agent 做,八成会越做越乱;第二个项目如果只做死工作流,又会显得特别笨。这就是我现在越来越在意的判断点:不是任务里有没有模型,而是路径到底稳不稳。
第一个项目教会我的,是“很多流程根本不需要自主规划”
文档处理那个项目刚开始时,也有人提议让模型自动决定每一步怎么做。听上去很先进:先读文档,再判断要不要抽摘要、要不要分类、要不要纠错、最后决定怎么入库。
但我们真正把流程梳理完之后发现,这件事根本没那么开放。
它的真实链路其实很固定:
- 读文档
- 提取正文
- 做字段清洗
- 生成摘要和标签
- 输出固定结构
- 入库
这里面真正需要模型判断的,其实只有个别节点,比如分类和摘要;主流程完全是确定的。
我们后来没有做 Agent,而是把主干写成工作流,再在少数节点上接大模型。结果非常稳。日志清楚,失败能重跑,出了问题也知道卡在哪一步。
落地的时候我们用的就是一个朴素的状态机串行执行器,每一步的输出落到一张任务表里,带 step 和 status 字段。模型节点被我封成一个普通函数,外面套上重试和超时,对调用方来说它和「调一个 HTTP 接口」没有区别:
1async def run_document_pipeline(doc_id: str): 2 ctx = {"doc_id": doc_id} 3 steps = [ 4 ("fetch", fetch_document), 5 ("extract", extract_main_text), 6 ("clean", clean_fields), 7 ("classify", classify_with_llm), # 模型节点 8 ("summarize", summarize_with_llm), # 模型节点 9 ("persist", persist_result), 10 ] 11 for name, fn in steps: 12 await mark_step(doc_id, name, "running") 13 try: 14 ctx = await fn(ctx) 15 await mark_step(doc_id, name, "done") 16 except Exception as e: 17 await mark_step(doc_id, name, "failed", error=str(e)) 18 raise # 让上层决定从哪一步重跑
这套东西最大的好处是「确定性」。同一份文档跑两遍,除了模型节点的措辞有点差别,链路完全一致。后来线上真出过一次问题:extract 阶段对某类扫描件 PDF 提不出正文,结果一查日志,failed 卡在哪一步、error 是什么一目了然,补一个分支就修好了。要是当时让模型自己「决定要不要提取正文」,这种问题排查起来会是噩梦——你根本不知道它今天是判断成不需要,还是真的失败了。
模型节点我还专门加了输出校验。分类必须落在预设标签集合里,摘要必须能被 JSON 解析,不满足就重试一次、再不行就降级走确定性规则判断。早期没做这层,模型偶尔会在标签前面加一句「根据文档内容,我认为分类是:」,直接把下游入库的枚举字段污染了。这种坑踩过一次就长记性。
这次经历让我第一次明确地感觉到:多步骤不等于 Agent。一个任务就算有六七步,只要路径稳定,它本质上还是流程编排问题。
第二个项目让我看到,工作流也会有明显上限
运维助手那次就不一样了。
用户会问:
- “帮我看看这个服务为什么最近报警变多了”
- “对比一下昨天和今天的错误日志趋势”
- “如果我要排查支付回调失败,应该先看哪几个系统”
这种任务的特点是目标清楚,但路径不固定。不同问题,第一步就可能完全不同:
- 有的要先搜知识库
- 有的要先查监控
- 有的要先拉日志
- 有的还得根据前一步结果再决定下一步
我们最早也尝试过工作流化,结果非常别扭。因为你很难提前把所有路径都列出来。流程一旦写死,助手就会表现得像一个菜单系统:能做的事都在预设分支里,一旦用户问法稍微拐一点,就开始答非所问。
我印象很深的一个反例:用户问「支付回调失败先看哪几个系统」,我们预设的分支只覆盖了「查日志」和「查监控」,结果它愣是把这个排查类问题往「查日志」上硬塞,回了一堆原始日志,半句有用的判断都没有。当时同事还想再加一个分支,我拦住了——你加得过来吗?真实排查路径是组合爆炸的,今天加「先看网关再看队列」,明天又来个「先看下游商户配置」,分支表会被撑成一坨没人敢动的 if-else。
这时候 Agent 的价值才真正出来。不是因为它“更高级”,而是它确实更适合这种需要边走边判断的任务。
后来这个助手我们是用 ReAct 那一套思路做的:给模型一组工具描述,让它在「思考—调用工具—看结果—再思考」的循环里自己推进,直到给出结论或者触发上限。大致是这样:
1SYSTEM = """你是运维排查助手。可用工具见 tools。 2每轮只能调用一个工具,拿到结果再决定下一步; 3信息足够时直接给结论,不要硬凑步骤。""" 4 5async def run_agent(question: str, max_steps: int = 8): 6 messages = [{"role": "system", "content": SYSTEM}, 7 {"role": "user", "content": question}] 8 for _ in range(max_steps): 9 resp = await llm.chat(messages, tools=TOOLS) 10 if not resp.tool_calls: 11 return resp.content # 模型认为可以收尾了 12 call = resp.tool_calls[0] 13 result = await dispatch_tool(call.name, call.arguments) 14 messages.append(resp.as_message()) 15 messages.append({"role": "tool", 16 "tool_call_id": call.id, 17 "content": truncate(result, 4000)}) 18 return "已达到最大步数,请人工接管" # 硬顶,绝不无限循环
max_steps 这道硬上限看着土,但它救过我好几次。没有它的时候,模型偶尔会在两个工具之间反复横跳——查完日志觉得不够,去查监控,又觉得日志没看全,再回去查日志,token 烧得飞快还出不来结果。给探索型循环装个硬上限,是我现在写 Agent 的第一条肌肉记忆。
我现在判断 Agent 和工作流,基本看三件事
第一,看路径是不是稳定。
如果一个任务 80% 情况都走同样的步骤,那优先工作流。别为了“智能感”把稳定流程交给模型自由发挥。
第二,看失败成本高不高。
如果一步错了会影响订单、权限、资金、客户数据,那主干流程必须尽量写死。模型可以辅助判断,但不该掌握整个执行权。
第三,看任务是否要求探索未知信息。
如果模型需要先去搜、再比对、再决定下一步,甚至中途可能修正原计划,那才更像 Agent。
这三个问题我现在几乎每次都会问一遍,因为它们能很快帮我把“看起来都像 AI 自动化”的需求拆开。
后来我又加了两个问题。
第四,看动作是否可逆。
如果模型只是查资料、整理日志、生成建议,试错成本还可控。但如果它要改配置、发通知、提交审批、关闭告警,那就必须考虑撤销、补偿和人工确认。
第五,看中间状态是否需要持久化。
简单问答可以一次性完成,复杂任务往往要跑多步。任务跑到一半失败,系统能不能恢复?用户刷新页面后还能不能继续?这决定了你是在做“模型回答”,还是在做真正的任务执行系统。
很多团队会误把“多步工具调用”当成 Agent
这是我最近最常看到的一个偏差。
有些系统本质上只是:
- 读输入
- 调一个接口
- 调另一个接口
- 按模板输出结果
这当然是多步,但不等于 Agent。它没有动态规划,也没有真正的路径选择,只是流程比单步长了一点。
如果把这类流程也包装成 Agent,短期可能显得很先进,长期问题很多。因为一旦让模型自由控制原本可以确定的步骤,你就会开始面对一连串新问题:
- 工具参数为什么变了
- 为啥这次少走了一步
- 为什么昨天成功今天失败
- 失败后该从哪儿恢复
这些问题本来可以靠流程约束直接规避。
举个具体的:我们早期有个「同步用户数据」的需求,三步——拉源数据、转换、写目标库,路径死板得不能再死板。当时有同事非要给它套个 Agent 壳,理由是「以后可能加步骤」。结果上线第二周,模型某次抽风把「转换」和「写库」的顺序调了个个儿,往目标库写了一批没清洗的脏数据。这种事在写死的工作流里压根不可能发生,因为顺序根本不归模型管。所谓「以后可能加步骤」,真要加的时候改一行 steps 列表的成本,远比为它担惊受怕一整年低。
我现在会把自动化任务粗略分成三层:
1固定工作流:路径确定,模型只处理某些语义节点 2半开放工作流:主干确定,局部允许模型选择工具或分支 3Agent:目标确定,但路径需要模型动态规划和修正
大多数业务项目其实落在前两层。全自主 Agent 很少是第一版就该上的方案。先把主干流程、工具协议、失败恢复做扎实,再逐步放开局部自主权,通常更稳。
到了真实系统里,Agent 的难点从”会不会想”变成了”出了事怎么处理”
这个认知也是我做第二个项目时被逼出来的。
在 Demo 环境里,Agent 往往看起来很聪明。它会拆任务,会归纳,还会自己决定先调哪个工具。可一旦进入真实系统,你马上会碰到完全不同的问题:
- 工具返回半结构化垃圾怎么办
- 上一步失败后,状态存在谁那里
- 一个任务执行到一半中断,能不能恢复
- 模型选错工具时,系统能不能发现并纠正
这些问题不是“提示词再写好一点”就能解决的,它们属于工程治理问题。
所以我现在看一个 Agent 系统,会特别关注这些东西:
- 工具是不是封得干净
- 每一步是否可观测
- 中间状态有没有地方落
- 失败后是否能人工接管
这些事情如果没做好,Agent 再能说,也只是一个容易失控的黑盒。
这里面最容易被低估的是状态机。
一个 Agent 任务不应该只有“执行中”和“结束”。至少要能表达:
1planning 正在拆解任务 2waiting_tool 等待工具结果 3waiting_user 等待用户确认 4running 正在执行动作 5failed 执行失败,可重试或人工接管 6completed 已完成 7cancelled 已取消
状态清楚以后,恢复和排查才有基础。否则任务卡住时,你只知道“Agent 没反应”,不知道它是在等工具、等用户,还是已经失败。
我们后来把每一步的「思考过程、工具入参、工具出参、状态流转」全都落库,相当于给每个任务存一份完整 trace。这块当时觉得是额外工作量,上线之后才发现是回本最快的投入——用户说「它给我的建议不对」,我们能直接把那次任务的 trace 拉出来,看它到底调了哪个工具、拿到什么数据、在哪一步拐歪了。没有这个,所有线上问题都只能靠复现,而模型的输出本身就不稳定,复现概率感人。
工具这一层我也踩过坑。最早工具是直接把内部 SDK 包一层就丢给模型,参数又多又乱,模型经常传错。后来我把工具收敛成「窄接口」:每个工具只暴露模型真正需要的几个参数,危险参数(比如时间范围、环境、目标集群)要么写死、要么由系统注入,绝不让模型自由填。
1# 反例:参数太开放,模型容易乱传 2def query_logs(service, env, cluster, level, start, end, limit, regex): ... 3 4# 现在的做法:只留语义参数,其余由系统约束 5def query_logs(service: str, minutes: int = 30): 6 minutes = min(minutes, 120) # 上限钳死 7 return _query(service, env=CURRENT_ENV, level="error", 8 start=now() - minutes, end=now(), limit=500)
工具描述写法也有讲究。我现在会在 description 里明确写清楚「什么时候该用、什么时候不该用」,比让模型自己猜有效得多。比如查监控的工具我会注明「只用于看指标趋势,不要用它查具体某条请求的日志」,加了这句之后工具误选率肉眼可见地降了。
有副作用的任务还要有补偿动作。比如创建工单后又发现条件不满足,能不能关闭草稿?发通知前能不能先生成预览?改配置前能不能生成 diff?这些都比“让模型再想想”更重要。我们的硬规矩是:凡是有副作用的工具,一律两段式——模型只能生成「意图」(要改什么、改成什么),真正的执行由系统在拿到人工确认后做。模型这辈子都碰不到那个真正落库的写操作。
后来我越来越接受一种更务实的做法
很多项目其实不需要在“纯工作流”和“全自主 Agent”之间二选一。
更实用的方式通常是:
- 主干流程写死
- 把模型放在少数确实需要语义判断的位置
- 在开放型环节里再放开自主规划
比如文档项目就是工作流主导,分类和抽取节点交给模型。运维助手则是 Agent 主导,但工具权限、执行范围、输出格式仍然受系统约束。
这种做法没有那么“酷”,但很符合真实交付环境。它既不把模型压成死模板,也不把可控性一把丢掉。
我现在会把这种做法叫”限定范围内的自主性”。
模型可以规划,但只能在允许的工具集合里规划。模型可以建议执行,但高风险动作要确认。模型可以失败重试,但重试次数、退避策略和人工接管由系统控制。
1模型负责:理解目标、提出计划、选择低风险工具、整理结果 2系统负责:权限、幂等、状态、重试、审计、人工确认
这个分工一旦清楚,Agent 就不再是一个黑盒,而是一个被工作流约束住的执行单元。
还有两个上线后才会疼的点:成本和延迟
Demo 阶段没人在乎这个,真上了线才发现它能要命。
Agent 是多轮循环,每一轮都把之前的对话和工具结果全塞回去,token 是累加的。我们运维助手早期一个复杂排查能跑七八轮,单次成本是固定工作流的十几倍,月底账单出来才被吓一跳。后来做了几件事压下来:工具返回先截断和摘要再喂回模型(原始日志几千行没必要全进上下文),把稳定不变的系统提示和工具描述做 prompt 缓存,再给简单问题加一个快速分流——能用一次问答答的,根本不进 Agent 循环。
延迟也类似。串行的多轮工具调用,用户要干等好几秒甚至十几秒。我现在会尽量把无依赖的工具调用并发掉,并且做流式输出,让用户先看到「我在查监控…」这种中间状态,哪怕总耗时没变,体感也好很多。这些都是工作流模式下根本不用操心、一上 Agent 就躲不掉的账。
文档处理和运维助手这两个项目,现在看已经不像同一类问题了:前者主干早就定死,模型只在两个节点上说话;后者路径本来就没法提前列全,只能让模型边走边判断,剩下的靠工具权限和状态落库撑住。
很多项目不是做不出 AI,而是一开始就选错了自动化层级。主干应该被写死的地方,就老老实实写死;真正需要模型判断和规划的地方,再给它空间,把工具、权限、重试这些交给系统管住。这套分工想清楚了,系统才不会又慢又乱。