我叫沈青锋,创业八年,前两个项目做SaaS和物联网数据平台,第三个项目一脚踩进制造业,做AI落地。去年年底,我们给一家汽车零部件供应商做了个差旅报销自动化系统,用的是AWS Bedrock的多智能体协作。这套方案上线第一个月,审批周期从平均7.2天压到了1.6天,财务手工复核量降了40%。但上线第二周的凌晨四点,我被告警电话吵醒——系统自动批准了一笔8500元的超标住宿费,没经过部门总监签字。那条流水差点让财务经理把整个项目叫停。
这篇文章不是AWS产品手册的翻版,而是我从这个项目里扒出来的实战记录:怎么用Bedrock Agent把报销流程拆成三个子智能体,怎么在长事务里靠共享记忆和Step Functions兜住数据一致性,以及那些被LLM幻觉打脸、被成本账单吓醒的瞬间。
30秒速览
- - 用Bedrock多智能体拆解差旅报销,把审批拆成TripAgent、PolicyAgent、FinanceAgent三个子智能体,Supervisor动态路由,比硬编码工作流灵活数倍。
- - LLM幻觉会打穿自动审批链,必须用Pydantic等schema校验拦截错误输出,并兜底转人工审核,否则一个拼写错误就能跳过总监审批。
- - 多智能体会话状态持久化要自己做(DynamoDB),不要依赖内置会话;Step Functions负责刚性流程和 Saga 补偿,避免让LLM直接调用资金过账接口。
一家2000人工厂的报销之痛,逼着我们扔掉硬编码工作流
客户场景:五家工厂,四套系统,一个永远对不上的报销单
客户是一家给德系车企供传动轴零件的制造集团,在常州、合肥、重庆有五家工厂,总共2000多号人,其中经常出差的工程师、项目经理、销售大约400人。差旅报销流程横跨四套系统:携程商旅负责订票订酒店、用友NC做预算和费用核算、泛微OA管审批流程、SAP B1做最终过账。表面上OA有电子流,但实际上每一单报销都得由申请人打印行程单、发票,贴在A4纸上扫描上传,财务部五个会计每天花四小时核对三单(行程单、发票、审批单)的一致性,然后手工录入用友生成凭证。平均一单报销从提交到入账7.2天,月均发生1200单,其中约12%出现金额差异或政策不符,需要打回重提。到了旺季,财务部天天加班到九点。
他们IT总监找到我们的时候,目标很明确:能不能用AI把审批和过账做成全自动,人工只处理异常单?我们最初的反应很朴素——拿AWS Step Functions画个状态机,用Lambda调OA接口、调用友API,把规则写成代码,这不就完了?(延伸阅读:在90分贝噪音和2Mbps带宽下,我把GPT-5.5的多模态延迟压到了487ms)
硬编码工作流烧掉的半年时间和一个惨痛教训
我们真的这么干了。2023年一季度,我用三个后端工程师写了一套基于Step Functions的报销引擎:十几个Lambda函数串起携程API取行程、用友API查预算、OA API触发审批节点,最后生成SAP凭证。规则用Python的if-else写死在代码里,比如“工程师住宿标准每晚不超过400元”“总监及以上房型自动升级行政房无需审批”“跨城市交通费超过800元需要二级审批”。初期运行还算稳,但三个月后噩梦开始。集团发布新版差旅政策,住宿标准按城市级别分三档,还要区分淡旺季。我们改规则就得改代码、走CI/CD、全量回归测试,一次政策更新折腾一周。更要命的是,有些模糊场景代码根本覆盖不住——比如工程师出差去客户工厂,临时被安排住进对方协议酒店,房价超标但合理,硬编码流程只会一刀切拒绝,申请人得走人工申诉通道,体验极差。坚持了半年,IT总监跟我说:“你们这自动化比人工还僵化,算了吧。”
那次失败让我认清一件事:审批流程里有大量基于语义和上下文的判断,比如“这个超标是否合理”“这个费用描述是否和行程匹配”,传统的状态机+规则引擎根本兜不住。我们必须让系统理解费用背后的意图,而这是LLM的强项。2024年4月AWS推出Bedrock多智能体协作功能后,我立刻拉着团队重构。
把报销拆成三个Agent和一个Supervisor,状态路由差点被LLM幻觉打穿
多智能体拆解:TripAgent、PolicyAgent、FinanceAgent各司其职
我们基于Bedrock Agent的多智能体模式重新设计了引擎。核心思路是一个Supervisor Agent接收用户提交的报销请求,根据语义动态分配给三个子智能体:TripAgent负责核对行程单与发票的一致性,调用携程商旅API获取实际出行记录;PolicyAgent负责检查差旅政策的合规性,判断超标是否合理;FinanceAgent负责预算占用检查并生成过账预凭证,最终提交到SAP。每个子Agent都挂载了对应的Action Group,用OpenAPI schema定义可调用的Lambda函数。(延伸阅读:我用GPT‑4o升级版帮同事查了一个堆栈溢出的Bug,它画了张调用图,我直接沉默了)
下面是创建TripAgent及其Action Group的代码片段,用的是Boto3和Bedrock Agent的create_agent接口。
import boto3
import json
bedrock_agent = boto3.client('bedrock-agent')
# 创建TripAgent,基于Claude 3.5 Sonnet模型
trip_agent = bedrock_agent.create_agent(
agentName='TripAgent',
foundationModel='anthropic.claude-3-5-sonnet-20240620-v1:0',
instruction='''
你是一个差旅行程核对专家。你可以通过调用get_trip_detail来获取员工的实际出差记录,
包括城市、日期、酒店、机票信息。你需要比较报销单中的行程信息与实际记录是否一致,
并标记任何差异。输出格式为JSON,包含字段:
match_status: "match"|"partial"|"mismatch"
details: string
confidence: float
''',
agentResourceRoleArn='arn:aws:iam::123456789012:role/BedrockAgentTripRole',
)
# 定义Action Group,关联Lambda函数
trip_action_group = bedrock_agent.create_agent_action_group(
agentId=trip_agent['agent']['agentId'],
agentVersion='DRAFT',
actionGroupName='trip-verification',
actionGroupExecutor={
'lambda': 'arn:aws:lambda:us-east-1:123456789012:function:get_trip_detail'
},
apiSchema={
'payload': json.dumps({
"openapi": "3.0.0",
"info": {"title": "Trip API", "version": "1.0"},
"paths": {
"/trip/{employee_id}/{date}": {
"get": {
"summary": "获取员工出差记录",
"parameters": [
{"name": "employee_id", "in": "path", "required": True, "schema": {"type": "string"}},
{"name": "date", "in": "path", "required": True, "schema": {"type": "string"}}
],
"responses": {"200": {"description": "成功返回行程数据"}}
}
}
}
})
},
actionGroupState='ENABLED'
)
PolicyAgent和FinanceAgent类似,分别绑定查询政策规则和预算接口的Lambda。Supervisor的instruction里定义了路由规则:当输入涉及行程核对,转给TripAgent;涉及费用合规,转给PolicyAgent;涉及预算和过账,转给FinanceAgent。Bedrock多智能体编排会自动处理调用和状态传递,我们用enableAgentCollaboration打开多智能体模式,并把三个子Agent的Alias注册到Supervisor的collaborator list中。这套搭建只花了两周,比之前硬编码开发时间缩短了70%。
第一次上线就被幻觉打穿:预算检查跳过总监审批的真相
测试环境跑通后,我们选了20个历史报销单做影子运行,准确率91%,勉强达标。但上线第二天,一个销售提交了一笔8500元的住宿费,酒店是重庆某五星级,PolicyAgent接到Supervisor的校验请求后,返回了一个JSON格式错误的响应:本该是{“approval_needed”: true, “level”: “director”},结果模型在生成时把”approval_needed”拼成了”approveal_needed”,还漏掉了”level”字段。Supervisor解析响应时,因为没找到预期的字段,直接走了默认逻辑——认为不需要高级别审批,就把单子推给了FinanceAgent生成凭证。这笔钱在凌晨自动过账,直到财务经理早上看报表才发现。(延伸阅读:为什么我放弃了七套专用审核模型,用GPT-5.5一个多模态接口端到端重建内容安全流水线)
根因很清楚:我们把LLM输出的JSON当可靠的结构化数据,没做 schema 校验。那次事故之后,我们在每个子Agent的Lambda里加了一层Pydantic验证器,任何不符合预定义模型的输出都会被拦截并触发人工审核。下面是PolicyAgent Lambda中引入的校验逻辑:
from pydantic import BaseModel, ValidationError
from typing import Optional
class PolicyCheckResult(BaseModel):
approval_needed: bool
level: Optional[str] = None
reason: str
policy_rule_id: Optional[str] = None
def lambda_handler(event, context):
# 调用Bedrock Agent获取raw_output(假设已经拿到)
raw_output = invoke_policy_agent(event['expense_detail'])
try:
validated = PolicyCheckResult.parse_raw(raw_output)
except ValidationError as e:
# 发送告警,并返回安全兜底值:需要最高级别审批
send_sns_alert("PolicyAgent schema validation failed, manual review required.")
return {
"approval_needed": True,
"level": "CFO", # 兜底为最高审批层级
"reason": "Validation error, escalated.",
"policy_rule_id": None
}
return validated.dict()
我们还给PolicyAgent的instruction里加入了few-shot示例,规范输出格式。同时,在Supervisor的编排逻辑里加了第二条防线:如果任何子Agent返回的confidence低于0.85,或者返回异常状态,Supervisor会直接终止自动链,转人工OA节点。这个改动让幻觉导致的漏审概率降到了0,财务经理总算肯让我们继续试运行。
共享记忆和Step Functions的长事务兜底,我们没丢过一单
用DynamoDB会话记忆跨Agent传递报销上下文
多智能体协作的另一个核心挑战是长事务上下文。一个报销单从提交到过账可能要经过三四个Agent轮转,中间还可能有人工审批中断,必须把会话状态持久化。Bedrock Agent提供sessionAttributes参数,可以在每次invoke时传入键值对,我们直接用DynamoDB表存储每个报销单的完整状态:expense_id作为Hash Key,包含当前环节、各Agent的结果、审批链状态、时间戳等。每次Supervisor调用子Agent时,会把sessionAttributes带上,子Agent返回后,Supervisor更新DynamoDB。(延伸阅读:GPT-4o升级版把推理藏进了黑盒,我却用它反编译了它的思考过程)
这里没有花巧,但教训就是:不能依赖Bedrock的内置会话记忆做长事务,它的超时和容量限制对业务流程不够可靠。我们遇到过内置会话在人工审批挂起超过两小时后被清除,导致后续Agent拿不到前序上下文。迁移到自管理DynamoDB后,所有中间结果可追溯,即使审批中断三天,回来后仍能继续流转。
Step Functions负责最终一致性,Lambda负责执行外部API
有人会问,既然Bedrock多智能体这么强,为什么还要用Step Functions?我们的分工很明确:Bedrock负责意图识别和动态决策,决定“该不该审批”“由谁审批”;Step Functions负责状态机的刚性流程,保证“审批超时怎么办”“API调用失败怎么补偿”。具体来说,Supervisor做出决策后,会把审批步骤写入Step Functions的执行输入,触发一个标准状态机。这个状态机管理审批生命周期:发送飞书通知、等待回调、超时自动升级、最终过账等。其中SAP过账的API调用放在Lambda里,被Step Functions task调用,如果过账失败,会触发Saga补偿——回滚用友预算占用并记录异常。
这种“软硬结合”让我们避免了纯粹依赖LLM控制流程的风险。Step Functions的可视化执行历史也让财务审计人员能清楚看到每一步的决策路径,而不用去翻Bedrock的原始日志。实际运行中,我们处理了14%的审批超时自动升级,以及2%的SAP API瞬时故障重试,没有丢失一张单子。(延伸阅读:Optimus分拣仿真99.2%,实测71.3%——我复现端到端模仿学习后,发现Sim2Real的三个死穴)
账单、安全和日志的实战教训,我们交了$4000学费
成本失控的第一个月:多智能体调用怎么吃掉$4000
项目刚上线的第一个月,AWS账单出来我吓了一跳:单月Bedrock模型调用费用$4,127,而Lambda和Step Functions加起来不到$300。排查发现,每处理一个报销单,Supervisor会平均调用4.2次Claude 3.5 Sonnet,三个子Agent各调用1-2次,总tokens消耗约8K。1200单乘以8K tokens,加上我们在调试时无限制跑历史数据对比,直接把成本打爆。我们立即做了三个优化:一是在PolicyAgent前置一个规则引擎快速过滤80%的标准政策检查,减少不必要的LLM调用;二是对Supervisor的prompt做了裁剪,去掉冗余描述,把平均tokens降到5.2K;三是设置Budget Alert,单日费用超过$100就自动暂停非核心Agent。优化后月费用稳定在$900左右,相当于雇佣一个初级会计月薪的三分之一,ROI成立。
日志审计:记录每一次LLM的输入输出,救了我们一次合规审查
制造企业非常看重合规。系统上线第二个月,集团内审部门要求抽样检查10笔自动审批的差旅单,看是否符合SOX要求。我们原本只记录了Bedrock Agent的调用摘要,审计员追问:“你是怎么判断这笔超标是合理的?”我答不上来,因为没保存完整的prompt和completion。后来我们通过CloudWatch Logs记录每一次bedrock:InvokeModel的完整请求和响应,并设置保留期13个月。同时,我们在DynamoDB里记录了每个报销单对应的所有Agent决策轨迹,包括原始LLM输出和Pydantic校验后的最终值。这个改动在一次外部审计中直接证明了系统的判断依据,避免了可能的合规风险。
权限与安全边界:不要让LLM直接写数据库
踩过的另一个坑是权限设计。早期为了方便,我们让FinanceAgent绑定的Lambda具有直接插入SAP凭证的权限。结果有一次Supervisor误将一条未完成审批的单子标记为“已批准”,Lambda直接生成了真实凭证,幸好当天财务对账发现了异常。之后我们严格遵循最小权限原则:所有涉及资金过账的操作必须通过Step Functions状态的转变触发,且只有经过人工审批或预设阈值以下的小额自动单才能进入过账状态。Lambda的IAM角色被拆分为只读查询和读写操作两类,读写操作还需要额外的MFA签名验证。安全边界清晰后,再没出过乱写凭证的事。
这套系统跑了大半年,处理了9000多张报销单,财务部从5人缩减到3人(另外两人转岗做成本分析),审批周期压缩到1.6天,错误率从12%降到1.8%。不算完美,但当财务经理说“终于不用天天加班贴发票”的时候,我知道这次的方向对了。用AI做审批,最难的不是模型本身,而是怎么让不可控的LLM输出,变成可控的业务决策链。如果你也在用Bedrock多智能体做类似的事,欢迎跟我聊聊你踩过的坑。