ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

大模型复杂流程调用实战:从API调用到稳定系统组件的工程指南

大模型复杂流程调用实战:从API调用到稳定系统组件的工程指南

很多人第一次接触大模型,都是从“调一个接口、拿一段回复”开始的。但随着业务真正落地,事情很快就会朝另一个方向演变——你不再只是“问一个问题”,而是要在一个完整的业务流程里调用模型,而且往往是很多次调用。这个转变,才是从“会用API”到“能做大模型应用”的分水岭。

复杂流程调用大模型,核心难点不在于怎么写代码,而在于怎么把一个不可控、有延迟、会出错、烧token的东西,用得像一个稳定、可靠的系统组件。这篇文章主要讲两件事:复杂流程里调用大模型会踩到哪些坑,以及我实际项目中沉淀下来的一套可复用的处理思路。适合那些已经能写通基本API调用、但正在被多步骤流程、长文本处理、并发任务折磨的开发者。不需要你有很深的算法背景,但最好已经写过一些调用大模型的代码。

1. 复杂流程调用大模型的整体设计思路

1.1 什么是“复杂流程”,它复杂在哪

先定义一下什么叫“复杂流程”。根据我经历过的项目,大致有这么几类典型形态:

第一类是长文本处理。你拿到一份几十页的文档,想让它帮你做摘要、提取要点、拆分章节。但模型上下文窗口有限,你没法一次把所有内容塞进去,只能分批读取、分段处理、再汇总。这类流程看着简单,实际做起来到处都是细节:分段边界怎么切、摘要怎么合并、中间结果存哪里。

第二类是多步骤任务链。比如让你写一份完整的市场分析报告,合理的流程是先让模型生成大纲,再根据大纲每一节单独生成内容,最后统一润色。每一步的输出是下一步的输入,中间任何一步出问题,后面全部跟着歪。

第三类是条件分支决策。比如用户提交了一笔售后请求,你需要让模型先做分类判断,再根据分类结果走不同的处理路径。这类流程的关键是“判断必须可靠”,因为模型的判断结果直接影响后续业务分支走向。

第四类是并发批量任务。比如电商平台有几千条商品评论需要做情感分析,每条评论相互独立,适合并行处理,但并发量一高就会触发限流,效率和稳定性之间需要做取舍。

你会发现,无论哪种形态,它们都有一个共同点:你不能把大模型当成一个普通的函数来调用。普通函数输入输出是确定的,怎么调用都不会出错,但大模型是一个概率系统,同样的输入可能给出不同的输出,可能解析失败,可能超时,可能触发了内容安全策略,可能给你返回一大段废话而不是你要的JSON。复杂流程要做的事情,就是在这个不稳定的“黑盒”外面,搭建一个稳定、可控的“壳”。

1.2 流程调用的核心原则:把模型当原子能力,而不是大脑

很多人在设计复杂流程时容易犯一个错误:让模型承担太多职责。比如在一个上下文里塞入所有历史信息,让模型自己“判断下一步该干什么”。这样做的结果往往是灾难性的——几轮之后上下文越滚越大,模型开始“忘事”,开始胡言乱语,而且出了bug你根本没法定位是哪一步出了问题。

我的做法刚好相反:把模型调用的粒度做小,每次调用只干一件明确的事。每个调用相当于一个“原子能力”,做的事情越单纯,输出越容易预判,出错率越低,重试和兜底也越好设计。而流程本身的逻辑,比如“先做什么、再做什么、如果结果不满足条件怎么办”,全部由代码来控制,绝不交给模型。

这就像流水线生产。你让一个工人从头到尾装配一台汽车,效率和合格率都很低;但如果你把它拆成若干个工位,每个工位只装一个零件,每个工位有明确的质检标准,整条产线的稳定性和可维护性就彻底不一样了。复杂流程调用大模型的本质,就是设计一条这样的大模型“流水线”。

2. 从单次调用到流程编排:链式、分支与聚合

2.1 链式调用:前一步的输出就是后一步的输入

链式调用是最常见的流程形态。拿“写报告”来说:先生成大纲,再三段式填充正文,最后统一润色。每一步的输出质量决定了下一步的效果,所以每步之间最好做一个简单的“质量关卡”。

我在这里吃过一次亏。早期做某个内容生成流程时,大纲生成完,直接拿它的全文输出往下传,结果某次模型抽风,大纲里混入了“(这里需要补充XXX数据)”这样的占位符,导致后面几个步骤全都围绕占位符生成内容,最后的报告完全不能用。从那以后,我在每一步之后都会加一个“格式校验和清理”节点,用一段正则表达式扫描输出,把无关信息过滤掉再传下一步。

链式调用的一个关键设计是步骤间的数据结构要明确。不要把上一步的原始输出直接作为下一步的输入,而是先解析成结构化字段。比如大纲步骤的输出虽然是文本,但我要求模型按固定格式输出(章节号+标题+一句话描述),然后在代码里解析成列表,下一步根据这个列表逐条处理。这样即使模型某次输出格式略有偏差,代码层的兜底逻辑也能修正。

2.2 扇出与聚合:并行化的正确姿势

再看扇出与聚合。以长文档分析为例,几十页的文档没法一次性处理,正确的思路是:先把文档切成若干段落,把每个段落发送给模型做局部摘要,再把所有摘要合并,让模型生成整体总结。切分-处理-聚合,这是复杂流程里最高频的组合模式。

并行化能明显提升吞吐,但不能无脑并发。原因有两个:一是供应商API基本都有QPS和并发数限制,超了直接报429;二是每个并发请求都在消耗资源,如果其中有一步需要重试,重试的拥塞还会加剧限流。我的经验是:先按照供应商文档的限额砍一半作为初始并发值,跑一轮压测再做调整。

对于聚合步骤,有个容易踩的坑:中间结果过多时,直接全量拼接。假设你有一百个段落摘要,每个摘要200字,合并起来就是两万字,再让模型做“总结”,效果反而不如随机抽样或按章节聚类后处理。所以实践中我会给聚合步骤设置数量上限,超过限量就先做一轮“摘要的摘要”。

2.3 分支决策:让模型“判断”,让代码“决定”

分支决策流程的核心陷阱是:模型判断结果不够稳定,而你却让它直接决定业务流程的去向。比如让模型判断一笔订单是否属于恶意退单,模型说“属于”,你就直接执行拦截。做过这行的人都懂——模型判断失败是常态,不是异常。

正确的做法是分层设计。模型负责给出判断和置信度,代码负责根据置信度决定走哪条路径。举个例子:呼叫中心质检流程中,模型先判断客服话术中是否有违规风险,输出“风险等级+原因”;代码拿到结果后,只有“高风险”才转人工复核,“中风险”先走自动提醒流程,“低风险”直接归档。人工复核永远是兜底。这样即使模型判断错误,也不会造成不可挽回的业务影响。

3. 上下文、状态与记忆:流程的“骨架”怎么搭

3.1 你管理的不是对话,而是数据

复杂流程中“上下文”这个概念,和聊天场景中的“对话历史”完全不是一回事。聊天时你可以把消息列表一股脑传进去,但在流程里,如果每一步都把完整历史传进模型,token消耗会爆炸式增长,而且模型注意力会被无关信息稀释,回答质量不断下降。

我的方法是:把上下文拆成两层——持久化状态和短期记忆。持久化状态是流程运行到当前节点所必需的关键信息,比如用户提交的表单数据、前面步骤产出的中间结果,这些放在数据库或内存存储里,随时可以读取。短期记忆是当前这次模型调用需要的即时信息,比如“上一步摘要结果”“用户最新一次输入”。

这样做有个明显的好处:任何一步出错,你都可以从持久化状态里恢复,重新执行出错的那一步,而不是从头开始整个流程。这在长流程里非常重要。曾经有个批次处理流程需要运行十几分钟,如果没有状态持久化,中间任一请求失败就得全部重来,那成本完全不可接受。

3.2 长流程的上下文压缩:别让历史成为负担

当流程步骤真的很多时,即使只传递关键信息,上下文也可能变得很长。比如一个多轮文书生成流程,前几轮修改意见已经十几万字,后面还要基于所有意见做最终版本,此时就需要“压缩”。

我有两种常用的压缩策略。第一种是摘要式压缩,让模型把累计的修改意见合并成一份结构化的需求清单,只传清单不传原始意见。第二种是结构化提取,把散落在多轮对话中的关键信息(诸如最终截止日期、必选项、排除项等)提取成固定字段,存下来,后续调用只传递这些字段。

这两种策略的本质是一样的:用更小的信息量保留对后续步骤最关键的语义。表现出来就是流程跑的更快、更省钱、错误率更低。建议你做一个经验法则:当流程中某一步的输入token超过模型的1/3窗口大小时,就该考虑压缩了。

3.3 中间结果的存储选型

流程的中间结果应该存哪里,很多人不重视。我常用的选择有三种:

  • 临时性的、量大的中间结果,用Redis这类内存存储,设置过期时间,流程结束后自动清理。
  • 需要长期追溯的记录,比如每次调用的输入输出、token消耗、耗时,写入结构化日志系统或数据库。
  • 供后续流程读取的二进制或长文本,直接落对象存储或本地文件系统,记录路径引用。

前两种最常见。特别建议每个中间结果都附带一个“流程实例ID”和“步骤ID”,这样一旦线上出问题,你可以按ID把整条调用链还原出来。

4. 结构化输出:让模型老老实实返回JSON

4.1 为什么自然语言不适合做中间产物

复杂流程中的中间产物,绝不能是自然语言长文,必须是结构化数据。就拿前文说到的“报告大纲”举例:你需要的是一份章节列表,每章节有标题和描述。如果模型输出一大段散文描述大纲,你后面没法程序化处理。

所以我在设计提示词时,会在结尾明确要求模型“以JSON格式返回”,并把期望的字段结构和示例给出来。示例怎么写特别关键。只给字段定义没用,模型经常理解不了抽象定义;你给一个“漂亮的示例”,它照着示例的结构返回的成功率会大幅提升。我甚至会把示例放在提示词的最后,紧跟着输出要求,实测下来格式命中率能提高不少。

4.2 JSON解析的实战技巧与兜底

但哪怕提示词写得再漂亮,模型偶尔还是会不老实。我遇到过返回空内容、返回被markdown代码块包裹的JSON、在大括号之外多加一行“以下是结果”、把值里的双引号转义搞乱等状况。

针对这些,我的兜底链路是:

  1. 先做清洗:用正则去掉代码块标记,去掉首尾的多余字符。
  2. 再做定位:找不到标准JSON时,尝试找到第一个{和最后一个},把中间内容提取出来再解析。
  3. 如果清洗后依然解析失败,重试,但会修改提示词,附上错误信息,让模型“重新输出严格的JSON”。
  4. 重试两次仍然失败,直接标记该步骤失败,走降级或人工处理流程。

这套链路在项目里跑了很久,格式解析成功率基本能稳定在99%以上。注意,千万不要只写裸的json.loads不做任何兜底——在流程里这等同于埋雷。

4.3 一个实用的结构化调用模板

这里给一个我经常用到的最小模板,思路比代码本身更重要:

import json from typing import Dict STRUCTURE_GUIDE = """ 请严格按照以下JSON格式返回,不要输出任何其他内容: { "summary": "一段不超过50字的摘要", "risk_level": "high|medium|low", "reason": "判断理由" } """ SYSTEM_PROMPT = "你是一个文本分析助手,只输出JSON。" def call_llm_with_json(user_prompt: str) -> Dict: # 构造请求 messages = [ {"role": "system", "content": SYSTEM_PROMPT}, {"role": "user", "content": user_prompt + "\n\n" + STRUCTURE_GUIDE} ] raw = llm_api_call(messages) # 清洗:去掉代码块标记 cleaned = raw.strip().strip("```json").strip("```") # 尝试补一对缺失的{},避免模型只返回内部字段 if not cleaned.startswith("{"): start = cleaned.find("{") end = cleaned.rfind("}") if start != -1 and end != -1: cleaned = cleaned[start:end + 1] # 解析 try: return json.loads(cleaned) except json.JSONDecodeError: return {"error": "parse_failed", "raw": raw}

这个模板跑了一阵之后,我真正体会到了“输出约束”对下游流程的巨大价值。json字段少一个,代码早崩一次,修复成本低得多。

5. 可靠性工程:重试、超时、限流与降级

5.1 哪些错误值得重试,哪些不应该试

复杂流程里,错误和异常是家常便饭。但很多人对重试的理解就是“错了就再发一次”,这其实很危险。不同错误的重试策略,要区别对待。

值得重试的错误基本是这三类:限流(HTTP 429)、暂时性服务不可用(5xx)、网络超时。这类错误的共同特征是系统状态没有变化,重试有机会成功。

不值得重试的也有三类:参数验证错误(说明你的请求本身有问题,重试一万次也一样)、内容安全拦截(模型拒绝生成,常以4xx或特定错误码返回)、认证失败。这类错误重试只会浪费时间和配额。更重要的是要记录错误原因,反馈给上游修正。

重试还要控制次数和间隔。我在项目里用的是“指数退避+抖动”:第一次重试等2秒,第二次4秒,第三次8秒,最多重试三次;为了让高并发场景下避免所有请求同时唤醒,每次间隔加一点随机偏移。

5.2 超时控制的整体策略

大模型调用延迟本来就不低,复杂流程里串联多次调用,总耗时很容易超出业务容忍线。所以超时要分层控:

  • 单次调用超时:从发起请求到拿到完整响应,通常是60~120秒。流式模式下,首帧时间一般不超过15秒。
  • 整体流程超时:整个流程最多跑多少时间,比如5分钟,超了就终止并触发补偿逻辑。
  • 步骤间等待超时:尤其异步任务场景,轮询结果时避免无限等下去,设定最大轮询次数。

单次调用超时的设置需要平衡模型能力和业务要求。设短了,模型生成一半就断;设长了,用户等不起。实际中我会先跑一批基准测试,摸清不同任务的P95延迟分布,再反推超时阈值。

5.3 降级方案才是稳定性的关键

我经历过最惨痛的一次事故:一个内容审核流程完全依赖大模型判断,当天供应商API发生大规模故障,整个审核队列堵死,业务将近两小时整个停摆。从那以后,我养成了一个习惯——每个关键节点都设计降级方案。

降级不一定要多复杂,常见的有这么几档:

  1. 缓存降级:如果同样的输入之前处理过,直接返回历史结果,完全不经过模型。
  2. 小模型降级:大模型不可用时,切换到响应更快但能力略低的小模型,保基本功能。
  3. 规则降级:大模型判断不可用时,用关键词规则做粗略判断,宁可误判也不要漏判,等大模型恢复后再人工复核。
  4. 纯人工兜底:把失败任务放进人工队列,由人工处理。

降级方案一定要提前设计、提前测试。临时抱佛脚是来不及的,故障发生时你根本没时间边排查边设计流程。

5.4 幂等性和并发控制

流程中遇到“调用超时但实际服务器已处理”这种模糊结果时,重试可能会导致重复处理。比如一个扣费判断流程,第一次请求其实已经成功,但因为客户端超时报错触发重试,就会重复判断一次。为了解决这类问题,我会为每次调用生成唯一的请求ID,重试时带上同一个ID。服务端如果支持ID去重,就能避免重复消费。如果不支持,至少可以在日志里把两次请求串起来,方便核对。

并发控制方面,除了前面说的设置并发上限,还可以为不同任务配置分级限流。比如实时单条请求和后台批量任务分开两套凭据,互不挤占,避免批处理把实时接口的配额打满。

6. 成本与性能优化:复杂流程跑得久更要跑得省

6.1 token的账,一定要会算

复杂流程调用次数多,token消耗很容易失控。很多供应商按“输入token数+输出token数”计费,而输入token里,上下文中的对话历史、系统提示词都会占地方。流程长了之后,有时候一次调用的输入token就能顶普通请求的十倍以上。

成本优化要从源头开始:让每个步骤的输入尽量精简。方法我前面已经说过,压缩历史、提取关键信息、不传无关上下文。另一个很有效的做法是精确控制max_tokens。模型经常会在任务完成之后继续“画蛇添足”,明明只要50个字,它给你写200字。把输出上限卡住,既省钱又减少了解析出错的概率。

6.2 缓存:同样的调用,没必要花两遍钱

语义级缓存的核心是:判断两个输入是否相似,相似就返回同一份结果。比如用户咨询“怎么重置密码”和“忘记密码怎么办”,对模型来说需要生成的答案高度相似,完全可以命中缓存。

精确匹配缓存实现简单,key就是输入+系统提示词的哈希值。语义缓存需要向量化+相似度阈值的判断,技术门槛高一些,如果业务量不大可以先不做。但精确缓存一定要做,尤其是在流程中有很多重复用户输入的场景下,收益非常明显。

6.3 模型分级与批量策略

不是每一步都需要最强的模型。复杂流程中,有些环节是简单的分类、抽取、格式整理,用小模型就能生成高质量结果,速度快且成本低;只有核心创作、深度分析这类环节才需要大模型。一开始偷懒全是默认大模型,账单出来后我就开始做分级,一个几十步的长流程跑下来,整体成本能降30%~40%左右。

供应商通常提供的批量接口价格低一些,但时效性不如同步接口。后台批处理任务的内容总结类任务可以走批量通道,把几批数据合并提交,跑完再统一拉取结果,成本降幅更可观。实时交互类任务不建议走批量,延迟不符合业务预期。

6.4 流式输出对流程体验的价值

感知延迟和真实延迟是两回事。一个流程如果总耗时有8秒,用户可能会觉得慢;但如果你用流式输出,1秒内就开始吐字,用户感觉到的等待时间会大幅缩短。我做的面向用户的流程几乎都开启了流式输出,尤其在步骤耗时较长的环节,把中间产出的部分结果直接推送到前端展示,产品反馈和用户体验比现有方案提升很多。

不过流式输出在复杂流程里要注意:流式接收的数据不能直接作为中间结果,必须等流结束之后再做结构化解析和存储。流式只解决体验问题,不改变流程本身的处理逻辑。

7. 可观测性:看不见的流程,没法被稳定运维

7.1 每次调用该记哪些日志

没有日志,线上出了bug你就只能盲人摸象。复杂流程里,每步模型调用都至少记录以下字段:

  • 流程实例ID、步骤ID、调用时间戳
  • 模型名称、系统提示词、用户输入摘要
  • 完整输出原文(至少存原始输出,便于后续追溯)
  • token用量(输入/输出)
  • 延迟时间、重试次数、最终状态
  • 错误码和错误信息,如果有的话

这里有个矛盾:完整记录输入输出会占用大量存储,但完全不记录后期没法排查。折中方案是:原始内容进归档存储,保留半个月;普通日志只有摘要和关键字段,长期保留。出问题时再去翻归档存储。

7.2 全链路追踪与评估

复杂流程是多个步骤的组合,单看某一步日志很难发现瓶颈。我一直沿用一条简单的追踪方案:给每个流程实例一个trace ID,每步调用都带上这个ID上报到日志平台,这样就能把一次完整流程的所有日志串起来。排查慢流程时,按trace ID查日志,一眼看到是哪一步耗了几秒。

评估环节也必不可少,尤其对模型输出质量的评估。变化所致的bad case里,无法判断是模型变弱了还是我们的提示词导致。我会为每个关键流程维护一个回归测试集,大概几十条代表性用例,在每次提示词改版之后批量跑一遍,对比输出质量和格式正确率。这一步看起来麻烦,实际是整个项目能稳定迭代做大的基础。

7.3 一个大致的质量观测指标参考

指标含义建议基线
格式合法率JSON解析成功率99%以上
重试率触发重试的调用占比低于2%
超时率单次调用超时占比低于1%
最终降级率走降级路径的任务占比越低越好
平均单流程token一次完整流程的token总量持续优化下降

重点关注趋势变化而不是百分数值。某一天格式合法率从99%掉到95%,说明提示词或模型行为发生了变化,需要立刻排查。

8. 常见问题与排查技巧实录

8.1 问题速查表

现象可能原因处理思路
模型偶尔返回非法JSON输出约束不足、上下文干扰加强清洗、增加重试、附上错误信息
流程总耗时很长串行步骤多并行化独立步骤、开启流式输出
并发一高就报429触发限流降低并发、指数退避、分桶限流
模型输出越来越“敷衍”上下文过长压缩历史、只传关键信息、摘要提取
某一步失败导致全流程挂掉缺少隔离和兜底给关键步骤加独立重试、降级方案
成本快速上涨上下文重复传入精简输入、加缓存、分级模型
处理结果不稳定提示词或模型版本变化维护回归测试集、固化提示词版本

8.2 一个完整的排查案例

某次影像描述流程突然频繁报错,现象是:大概每十次就有一次返回内容为空,流程直接中断。我先查昨天日志,这一步的空返回率从前一天的0.5%飙到11%,说明不是偶发而是系统性变化。再对比代码变更记录,发现提示词在前一天被调整过,新版提示词用了一个带JSON示例的长文本作为系统上下文。

根据经验,我判断不是模型服务故障,而是提示词结构发生了偏移,导致模型有时候把示例当成了完整答案返回。把这层信息传达给团队后,团队回滚了提示词版本,空返回率瞬间降到1%以下。这个案例给我的启示是:提示词的改动要和代码改动一样谨慎,任何上线动作都要有版本记录,否则出了问题根本不知道改了什么。

8.3 避坑清单汇总

最后汇总几条我在真实项目里淌出来的经验:

  • 在复杂流程中不要直接使用大模型的默认返回文本作为业务数据,一定要做结构化校验。
  • 无论多信任模型,永远给关键节点加人工兜底。
  • 提示词也是代码,必须版本化、可回滚。
  • 每一条外部依赖都要有降级方案,不提前准备,故障来临时一定会手忙脚乱。
  • 重试策略必须包含退避和抖动,否则高并发下重试风暴会把你整个系统拖垮。
  • 流程能拆就拆,不要把功能和判断全部揉进同一个调用里。步骤越独立,问题越容易定位。

写在最后的个人体会

做了不少把大模型接入业务流程的项目之后,我最大的体会是:复杂流程调用大模型,真正的技术难点其实不在“调用”本身,而在“容错”。模型大概率会在你最意想不到的时刻返回错误的内容、非法的格式,或者干脆不响应。你只能接受它出错,然后提前为每一种可能的错误准备好处理方案。顺着这个思路做下来,流程会越来越稳定,后期的维护成本也会降到让你惊喜的程度。

还有一个小技巧送给大家:在设计流程早期,就把日志和状态持久化做好。哪怕你觉得当前项目不会出问题,后续迭代时你也会在排查问题中体会到这项工作的价值。毕竟流程越复杂,越需要一个能让你看清全局的“仪表盘”。这些做扎实了,复杂流程就没有你想象中那么难搞。

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进