最近团队在 AWS 上搭 Kiro 这套 AI 原生工作流引擎,说实话,一开始我是有点低估它的。流程引擎我见过不少,从 AWS Step Functions 到 Temporal,但 Kiro 这种把 LLM 当成一等公民、把 Anthropic 那套"模型只是推理引擎"的理念直接揉进工作流定义的玩法,确实给了我不小冲击。这篇文章不打算讲 PPT 层面的架构图,只想把从理念到落地的关键路径,以及我们踩过的那些坑,原原本本记下来。如果你正在评估或者已经在用 Kiro 做 AI 工作流编排,下面这些内容应该能帮你省下不少调试时间。
先说明一下背景。Kiro 是我们内部对这套基于 AWS 的 AI 原生工作流编排服务的代号,它不只是一个"调模型"的工具,而是一个把模型调用、工具调用、状态传递、校验重试、可观测性全部打通的工作流运行时。我们选择它,是因为传统的 Step Functions 在编排确定性任务上很成熟,但面对"模型输出不稳定、上下文有长度限制、工具结果要反馈给模型"这类 AI 场景时,总感觉是在硬凑。Kiro 的设计思路更接近 Anthropic 官方推荐的应用框架:先定义好输入输出,再让模型在约束里做推理。这篇文章的核心,就是拆解这套设计,并给出可直接抄作业的工程实现。
1. 从 Anthropic 设计理念到 Kiro:先搞清楚"AI 原生"到底原生在哪
1.1 Anthropic 的设计哲学:模型是引擎,不是数据库
很多人用 Claude 这类模型时,还停留在"我写个 prompt,你帮我生成答案"的阶段。Anthropic 官方文档里反复强调的其实是另一件事:模型是一个推理引擎,它的输出质量高度依赖你给它什么样的上下文、工具和约束。你把所有资料都塞进 prompt,它就给"幻觉";你让它自由发挥,它就给你一篇格式乱七八糟的长文;你只在出错后重试,它大概率会重复犯同样的错。
Anthropic 在应用层的设计理念可以浓缩成三句话:
- 显式定义输入输出的 schema,让模型知道"你必须按照这个 JSON 结构返回",而不是靠 prompt 里一句"请用 JSON 格式"。
- 把工具当作模型的"手",模型负责判断该调用哪个工具、传什么参数,而工具本身要有一份严格的参数定义(JSON Schema),模型只是参数的填充者。
- 上下文是稀缺资源,你给模型看的每段文字都在占用它的注意力。上下文应该像数据库查询结果一样,按需取用,而不是把整个文档库都丢给它。
这些理念听起来不算复杂,但真正落到工程上,绝大多数团队还是在做"一个 prompt 走天下"的事。Kiro 的价值在于,它把这些理念强制变成了工作流定义里的结构字段。
1.2 Kiro 把理念翻译成了工程结构
我们第一次拿到 Kiro 的工作流定义文件时,第一反应是:这怎么长得有点像 CI 的 pipeline?但仔细看就发现,节点类型多了个llm节点,并且这个节点要求你必须声明schema和tools。这不是为了让 YAML 变长,而是把 Anthropic 的理念翻译成了约束。
对比一下传统工作流和 AI 原生工作流的差异:
| 维度 | 传统工作流 | Kiro/AI 原生工作流 |
|---|---|---|
| 核心计算单元 | 确定性的函数/任务 | 非确定性的 LLM + 工具 |
| 失败模式 | 异常、超时,可预测 | 输出不符合 schema、幻觉、上下文溢出 |
| 状态传递 | 显式传参,类型固定 | 需要携带上下文切片,且要管理 token 消耗 |
| 重试策略 | 直接重跑同一步 | 可能需要换模型、改 prompt、回退到规则逻辑 |
| 可观测性 | 日志记录参数和结果 | 需要记录 prompt、completion、tool call 全过程 |
Kiro 的工作流定义里,llm节点必须要配一个schema,这个 schema 不仅是输出校验用的,它还会被塞进模型的 system prompt 里,让模型从第一眼就知道自己该产出什么。同时,llm节点可以声明tools,Kiro 会自动把工具定义序列化成模型可以理解的格式。这些设计都不是 Kiro 发明的新东西,而是把 Anthropic 反复强调的最佳实践做成了平台默认能力。我们团队后来的经验总结就一句话:如果工作流里有个 LLM 节点却看不到 schema,那十有八九是要返工的。
2. Kiro 工作流设计的五个核心层次
2.1 模型网关层:路由规则与 fallback
AI 原生工作流要做的第一件事,不是写 prompt,而是把模型访问收敛到一个网关上。Kiro 的模型网关非常像一个微服务网关:客户端不直接指定"我要调 claude-3-opus",而是指定一个路由名,比如anthropic-main。网关根据路由名去查配置,决定真正请求哪个供应商的哪个模型。
这套设计解决了两个问题。第一个是供应商锁定:你今天用 Claude,明天想换成别的模型,只需要改网关配置,不用逐个改工作流文件。第二个是故障转移:模型服务总有不稳定的时候,网关可以根据路由规则自动 fallback 到备用模型。我们在生产环境里就遇到过 Anthropic API 区域性的超时,如果没有 fallback,整条工作流都会挂掉。
一个典型的路由配置长这样:
gateway: routes: - name: anthropic-main provider: anthropic model: claude-3-5-sonnet-20241022 timeout: 30s fallback: - name: kiro-local provider: local model: llama-3.1-8b - name: kiro-local provider: local model: llama-3.1-8b这里有个细节值得注意:timeout一定要设置。我们在初期没配超时,结果某些情况下请求会挂着两分钟才报错,整条工作流的执行时间被拖垮。后续我们统一把默认超时定为 30 秒,fallback 之后如果还是失败,才允许工作流进入重试或失败分支。
2.2 状态管理层:把上下文当作一等公民
工作流跑起来以后,最容易被忽视的就是状态管理。Kiro 把一次工作流执行看作一个有状态的run,每个节点都可以读写这个 run 的全局状态。但关键是,Kiro 并不会默认把所有状态都传给模型,这违背了 Anthropic 的上下文节约理念。
正确的做法是,在每个 LLM 节点里显式声明这个节点需要"看到"哪些状态字段。我们用过一个真实的例子:一个文档摘要工作流,前置节点已经用工具抽出了原始文本、作者、时间戳等一堆字段,但摘要节点其实只需要原始文本的前 8000 字符。如果你直接把全部字段拼接进 prompt,token 消耗会非常惊人,而且模型注意力会被无关信息稀释。
Kiro 里的做法是给状态打"切片"。比如:
- id: summarize type: llm inputs: excerpt: "{{state.raw_text | truncate(8000)}}" doc_title: "{{state.title}}"这样模型真正收到的上下文只有excerpt和doc_title,而不是整个 state。这种"按需组装上下文"的思路,就是 Anthropic 强调的 context engineering 在工作流层面的落地。
此外,状态管理还意味着要处理好执行上下文。每一条工作流执行都应该有独立的run_id,所有日志、指标、工具调用记录都绑定到这个run_id上。这样回头排查问题时,你可以完整回放一次执行的每一步,而不是靠各处日志里的时间戳去脑补。
2.3 工具接入层:schema 即契约
AI 工作流和传统工作流最大的区别之一是,模型会主动去调用工具。我们说"主动",其实并不神秘:你可以在 LLM 节点的配置里声明tools,然后填一个合适的 prompt,比如"如果你觉得需要查询订单信息,就调用 get_order 工具"。模型收到工具定义后,会在它的输出中生成一个 tool call。
Kiro 对工具定义的要求非常严格,必须使用 JSON Schema。你可以手动写,也可以让 Kiro 从 OpenAPI 文档里自动转换。重要的是,工具参数必须有明确的类型、必填项和描述。描述尤其关键,因为模型是靠描述来判断何时调用工具的。比如:
{ "name": "get_order", "description": "根据订单号查询订单状态、金额和物流信息。当用户询问订单相关问题时使用。", "parameters": { "type": "object", "properties": { "order_id": { "type": "string", "description": "订单编号,格式如 ORD-2025-001" } }, "required": ["order_id"] } }这段描述比你自己读给模型听更有效,因为它以结构化方式进入模型上下文。我们踩过的坑是:工具定义里有一项参数是布尔型,但 description 里写了"请传是或否",结果模型真的传了字符串"是",导致校验失败。后来我们把 description 改成"true 表示是,false 表示否",问题立刻消失。所以工具描述要避免自然语言模棱两可,尽量用枚举或者明确的值。
2.4 校验与重试层:接受模型会犯错
AI 工作流必须以"模型会输出错误结果"为前提来设计。这个前提听起来简单,但很多刚接触的人还是习惯性地认为,只要 prompt 写得足够好,模型就不会出错。实际上,哪怕是最强的模型,在复杂的输出格式上也时不时会漏掉一个字段。
Kiro 的做法是在每个 LLM 节点后自动做一次 schema 校验。校验失败时,可以选择重试、走 fallback 模型,或者直接进入人工处理队列。我们采用过的策略是:
- 第一次失败,带着校验错误信息重试一次(把错误信息拼进 prompt,让模型自我修正)。
- 第二次失败,切换 fallback 模型再试一次。
- 仍然失败,把这条执行标记为
failed_validation,写入死信队列。
这套策略的灵感也来自 Anthropic 官方文档里提到的 self-correction 模式。附带一个经验:重试的 prompt 里务必附上"你刚才的输出没通过校验,错误信息是……",而不是简单让它再来一遍。这样做效果立竿见影。
2.5 可观测性层:每次执行都要能回放
排错是 AI 工作流绕不开的日常。Kiro 默认会把每次执行的所有关键事件记录下来:模型请求和响应全文、工具调用参数和返回值、每个节点的耗时、状态切片的变化。这些记录统一以 span 形式导出到 OpenTelemetry,可以和 AWS X-Ray、CloudWatch 打通。
这部分的收益在使用前几天还感受不到,直到线上出现一次"模型返回了一个合法但完全错误的 JSON 字段",靠日志很难定位是哪个环节出的问题,因为工作流整体是成功的。后来我们用 OpenTelemetry 的 trace 把那次执行的每一步展开,发现是工具节点返回的数据本身就不对,模型只是按照错误数据生成了看似合理的摘要。如果没有完整的回放能力,这种问题基本没法查。可观测性不是锦上添花,而是 AI 工作流的必需品。
3. 从零搭一条可上线的"文档摘要归档"工作流
3.1 场景定义与前置条件
前面讲了不少设计层面的东西,现在进入实操。我们拿一个最常见的场景练手:上传一份 PDF,抽取文本,生成结构化摘要,提取关键字段,写入数据库。这个流程很多团队都做过,但用 Kiro 来做,最大的好处是每一步的输入输出都遵循 schema,模型乱输出的概率会大幅降低。
前置条件有这些:
- 一个 AWS 账号,并在目标区域开通了 Kiro 服务(实际部署时请按照真实服务名和区域查询)。
- 客户端安装了 Kiro CLI,或具备调用其 SDK 的权限。
- 已经有 Anthropic API Key,或至少有一个可用的模型网关地址。
我们在演示中会使用anthropic-main这个路由名,它指向 Claude 3.5 Sonnet。如果你用的是不同模型,记得把路由配置改成你自己的。
3.2 用 YAML 定义工作流
Kiro 的工作流定义通常是一个 YAML 文件。以我们的"文档摘要归档"为例:
workflow: id: doc-summary-archive version: 1 description: "从PDF抽取文本,生成摘要,提取结构化字段,存入数据库" nodes: - id: extract type: tool tool: pdf-extractor params: source: "{{trigger.uri}}" output: raw_text - id: summarize type: llm route: anthropic-main inputs: excerpt: "{{state.raw_text | truncate(12000)}}" prompt: | 你是文档分析助手。请阅读下面的文档片段,输出摘要和要点。 文档片段: {{excerpt}} schema: type: object properties: summary: type: string description: "不超过200字的摘要" key_points: type: array items: type: string description: "3-5个核心要点" title: type: string description: "文档标题" required: [summary, key_points, title] output: doc_summary - id: archive type: tool tool: db-write params: table: doc_summaries row: "{{state.doc_summary}}"几个关键点说明一下:
route: anthropic-main指向网关路由,而不是写死模型 ID。这样后续换模型不用改这个文件。inputs.excerpt从状态里取出原始文本并截断到 12000 字符,防止上下文超长。schema定义了输出结构。这个 schema 会同时用于两件事:注入模型 prompt、校验模型输出。
这就是我前面说"AI 原生"的意义:你的工作流描述的是意图和契约,而不是具体的函数调用链条。模型节点负责把自然语言文本变成符合 schema 的 JSON,工具节点负责真实世界里的副作用。
3.3 配置模型网关路由
要让上面的工作流跑通,网关里必须先有anthropic-main这个路由。配置方法一般不直接在 YAML 里写,而是通过 Kiro 的网关管理命令注册。命令行大致长这样:
kiro gateway routes create \ --name anthropic-main \ --provider anthropic \ --model claude-3-5-sonnet-20241022 \ --timeout 30s \ --fallback kiro-local创建完路由后,可以用kiro gateway routes list验证。如果路由没创建成功,后面工作流执行时会报"expected a gateway model route referee"之类的错误。这个我们放到下一节详细排查。
另外,API Key 之类的敏感信息不要写在 YAML 里。Kiro 支持从 AWS Secrets Manager 读取密钥,也可以从环境变量读取。我们团队的选择是 Secrets Manager,这样密钥轮换不需要改动工作流文件。
3.4 设置中文输出和本地化界面
关于中文设置,这是一个非常常见的问题。Kiro 的默认界面语言是英文,但如果你在 AWS 控制台里使用,可以在个人偏好设置里把语言切成中文;如果你用的是 Kiro CLI,可以通过环境变量指定:
export KIRO_LOCALE=zh-CNCLI 的日志、提示信息都会显示为中文。如果你是在自己开发的应用里嵌入 Kiro SDK,也可以初始化时传 locale 参数。
但需要区分的是:界面语言和模型输出语言是两回事。界面中文只是 UI 层面,而模型返回的摘要是否用中文,取决于你的 prompt 和 schema。我们在上面那个工作流里的 prompt 并没有明确要求中文,所以模型可能根据文档语言自动选择。为了稳定,建议在 schema 描述里写明"summary 字段请使用简体中文",或者在 prompt 里加一句"请始终用简体中文输出"。最稳妥的做法是在 few-shot 示例里给一个中文的例子。单纯靠"请用中文"这种指令,在长文本场景下偶尔还是会失效。
3.5 运行、验证与上线检查
工作流定义和网关配置都就位后,执行一次测试运行:
kiro runs start doc-summary-archive \ --param uri="s3://bucket/input/sample.pdf"执行过程中,可以用kiro runs get <run_id>查看每个节点的状态。如果成功,你会在状态里看到doc_summary是一个符合 schema 的 JSON 对象:
{ "summary": "本文介绍了一种基于AWS的AI原生工作流设计方法……", "key_points": ["模型网关是AI工作流的核心", "上下文管理决定成本和效果", "可观测性必须内置"], "title": "AWS Kiro AI原生工作流设计解析" }上线前我们还会做三件检查:
- schema 校验是否开启:确保每个 LLM 节点都有
schema,否则宁可多加一步人工校验,也不裸奔。 - fallback 是否有效:手动把网关路由改成一个错误的模型名,确认工作流会降级到备用模型,而不是直接失败。
- 预算控制:给每次执行设置 token 上限。Kiro 允许在路由上配置
max_tokens,建议设置,防止异常输入导致成本飙升。
这三件是我们在生产上吃过亏后才总结出来的。
4. 连接与路由问题排查实录
4.1 "unable to connect to anthropic services failed to connect to api.anthropic.com" 的根治思路
这个错误几乎是所有接 Anthropic API 的人都会遇到的。报错信息很直白:TCP 连接api.anthropic.com失败。但在不同环境下,根因差别很大。
我们团队遇到过的几种情况:
- VPC 内无法访问公网:Kiro 跑在 AWS 私有子网时,如果子网没有 NAT 网关,就无法访问 Anthropic API。解决方法是配置 NAT 网关,或者使用 AWS PrivateLink 接入 Anthropic 的终端节点。我们最终选了 PrivateLink,因为更稳,而且不用维护 NAT 的公网 IP。
- 出口 IP 不在白名单:有些企业账号在 Anthropic 控制台配置了 IP 白名单,只有白名单里的 IP 才能调用 API。此时需要在 VPC 上绑一个固定 EIP,并把它加进白名单。
- DNS 解析异常:在有些环境里,内网 DNS 劫持了外网域名。排查方法很简单,先试
curl -v https://api.anthropic.com看能否连通;如果连接失败,再试dig api.anthropic.com看解析结果是否正常。 - 代理配置冲突:如果系统环境变量里有
HTTP_PROXY/HTTPS_PROXY,Kiro 的底层 SDK 可能会走代理,而代理本身又不稳定。我们踩过这个坑,最后的处理是显式地在 Kiro 服务的环境变量里把代理清空,或者设置NO_PROXY包含api.anthropic.com。
排查这个错误时,我建议先从网络连通性入手,不要一头扎进 Kiro 配置里。先证明你这条机器能访问 Anthropic API,再谈其他。如果机器用 curl 能拿到响应,那问题大概率在 Kiro 的配置;如果 curl 也超时,那就是网络层问题。
4.2 "expected a gateway model route referee" 到底在说什么
这是我见过最让新手困惑的报错。完整错误像这样:doesn't look like an anthropic model: expected a gateway model route referee。
拆开来看:Kiro 的网关在做路由决策时,需要一个"裁判"(referee)来判断当前请求应该走哪条路。这个裁判就是路由规则。你工作流或代码里指定的模型引用方式,应该是一个已注册的路由名,而不是裸的模型 ID。当 Kiro 看到model: anthropic/claude-3-5-sonnet或model: claude-3-opus-4这种字符串,而它的路由表里又没有这个名字时,它就会说:"这东西看起来不像一个 anthropic 模型,我需要的是一个网关路由裁判。"
换句话说,这个错误不是说你调用了不存在的 Anthropic 模型,而是说你没有用网关能识别的路由名。解决步骤:
- 查看当前已有的路由:
kiro gateway routes list - 对比你的工作流或 API 请求里写的模型名。应该写路由名(例如
anthropic-main),而不是provider/model-id。 - 如果你确实想用某个新模型,先在网关里创建路由:
kiro gateway routes create \ --name claude-opus \ --provider anthropic \ --model claude-opus-4-20250514 - 再去改工作流里的
route: claude-opus。
这个错误还有个变种,就是 YAML 里route字段写对了,但网关配置里这个路由的provider字段写成了anthropic,而模型名是 Kiro 本地模型的 ID,导致网关无法建立一个匹配关系。所以创建路由时,一定要保证provider、model与你的模型服务商一致。
4.3 高频坑位速查表
最后把我们在 Kiro 实战中碰到过的问题整理成一张表,方便你排查时对照。
| 现象 | 可能原因 | 处理方式 |
|---|---|---|
| 模型返回内容完全符合 JSON 但语义不对 | schema 描述写得太宽泛 | 在 schema 的 description 里增加语义约束和示例 |
| 同一条工作流有时成功有时失败 | 上下文达到模型窗口上限 | 对文本字段做 truncate,或改用支持更长上下文的模型 |
| 重试仍然得到同样的错误输出 | 重试 prompt 没有附上错误信息 | 把上一次的校验错误拼接进重试 prompt |
| 工具调用总是失败 | 工具参数描述不明确 | 给参数加 enum 约束,或改写 description 中的自然语言歧义 |
| fallback 没生效 | fallback 路由不存在或配置错误 | 先手动调用 fallback 模型确认可用 |
| CLI 显示英文 | 未设置 locale | 设置KIRO_LOCALE=zh-CN并重启进程 |
| 网关路由创建报错 "route name exists" | 同名路由已存在 | 用update而不是create |
这张表的共性经验是:AI 工作流里的大多数问题,都不是模型不够聪明,而是边界没画清楚。你给模型的 schema、工具描述、路由约束越明确,模型的表现就越可预期。反过来说,上游任何一处模棱两可,都会在某个不确定的时刻以奇怪的方式暴露出来。
从 Anthropic 那套设计理念,到 Kiro 的工程实现,我最大的感受是:AI 原生工作流的重点不在"AI",而在"原生"。Kiro 没有把 LLM 当做一个黑盒函数来调用,而是把它放进工作流的每一个环节,同时用网关、状态、schema、可观测性这些经典的工程手段去约束它。这种做法看起来多写了很多配置,但长期维护下来的成本,远比一个"灵光一现"的巨型 prompt 低得多。如果你也在搭建类似的工作流,不妨先花一个下午把你的节点、schema、路由定义清楚,再去纠结 prompt 的措辞。顺序对了,后面会顺很多。