试了 8 种方式全失败后,我用双通道架构把 Kiro CLI 变成了 REST API
我最近在做一件"看起来不难但实际很折腾"的事:把 Kiro CLI 变成一个可以被其他服务调用的 REST API。
Kiro CLI 是亚马逊云科技推出的终端 AI 编码工具。代码生成、分析、重构,它都能干。但问题是------它只有 stdio 接口。你只能在终端里打字跟它聊天,没有 HTTP 端口,没有 SDK,没有任何可以被程序化调用的方式。
我想在自动化流水线里用它,怎么办?只能自己封装。
最终产物:两个文件,约 600 行 Python
先说结果,免得大家觉得是个大工程:
bash
kiro-api/
├── acp_client.py # ACP JSON-RPC 2.0 客户端(~300 行,纯标准库)
└── server.py # FastAPI HTTP 服务(~280 行)
FastAPI 起在 8642 端口,7 个 REST 端点,支持多轮会话和模型�"'换。
完整实现代码见下文。
搞清楚 ACP 协议
Kiro CLI 有个隐藏的子命令:kiro-cli acp。启动后通过 stdin/stdout 进行 JSON-RPC 2.0 双向通信,这就是 ACP(Agent Communication Protocol)。
主要方法:
| 方法 | 方向 | 作用 |
|---|---|---|
initialize |
Client → Kiro | 握手 |
session/new |
Client → Kiro | 创建会话 |
session/prompt |
Client → Kiro | 发任务 |
session/update |
Kiro → Client | 流式推文本(通知) |
session/request_permission |
Kiro → Client | 敏感操作审批 |
协议本身设计得不复杂。真正的坑在"它没告诉你的事"。
模型切换:8 种姿势,全军覆没
我想在 API 层支持动态切捡模型。按照正常思路,应该在协议里指定。
然而:
| 我的尝试 | 惨烈结果 |
|---|---|
session/new 加 model 参数 |
被直接忽略 |
session/setModel |
Method not found |
session/configure |
Method not found |
_kiro.dev/commands/execute/model |
进程炸了(反序列化错误) |
kiro-cli acp --model X |
unexpected argument |
kiro-cli-chat acp --model X |
unexpected argument |
环境变量 KIRO_MODEL |
不认 |
配置文件 ~/.kiro/settings/cli.json |
不认 |
8 种方式,无一幸免。 我当时的心情可以用"差点捠桌子"来形容。
分析了一圈发现:ACP v1.25 协议设计上就不支持运行时切捡模型。这是协议层的限制,不是我用法不对。
但还有一条路:kiro-cli chat --model X --no-interactive。这个命令支持指定模型,代价是只能一次性运行,不能多轮对话。
破局:双通道架构
一条路搞不定两个需求,那就开两条路。
scss
HTTP 客户端
|
| REST API (port 8642)
v
FastAPI Server
|
---+-------------
| |
| model=auto | model=指定
v v
ACP 通道 Chat 通道
(常驻进程) (一次性子进程)
多轮会话 单次调用
JSON-RPC 2.0 文本解析
| ACP 通道 | Chat 通道 | |
|---|---|---|
| 模型 | 不可选(auto) | 可指定 |
| 多轮 | 支持 | 不支持 |
| 冷启动 | 无 | ~3 秒 |
| 场景 | 日常多轮对话 | 指定模型的单次任务 |
设计原则很简单:能走 ACP 就走 ACP,只有指定模型时才降级到 Chat。
实现细节一:异步变同步
ACP 底层是异步 stdio。你往 stdin 写请求,stdout 上可能先推来一堆通知,然后才是你要的响应。
我用 Event + Pending Map 做了同步封装:
python
# 发请求:注册等待
self._pending[req_id] = (threading.Event(), [None, None])
# 写入 stdin
self._proc.stdin.write(json_msg + "\n")
# 阻塞等待
event.wait(timeout)
# 读线程:收到响应,按 id 匹配,唤醒
if msg["id"] in self._pending:
holder[0] = msg["result"]
event.set()
每个请求一个唯一 id,读线程负责分发,写线程阻塞等待。简单粗暴但有效。
实现细节二:文本块在通知里,不在响应里
这个坑花了我好几个小时。
session/prompt 的最终响应:
json
{"jsonrpc": "2.0", "id": 3, "result": {"stopReason": "end_turn"}}
没有文本! 我反复检查了好几遍,一度怀疑是不是哪里解析错了。
实际内容是通过 session/update 通知逐块推过来的:
swift
← {"method": "session/update", "content": {"text": "```python"}}
← {"method": "session/update", "content": {"text": "\ndef fibonacci(n):"}}
← ...更多文本块...
← {"id": 3, "result": {"stopReason": "end_turn"}}
正确做法:在列表里收集所有 chunk,收到 end_turn 后 join 拼接。
这个行为在官方文档里没写清楚。 只能自己抓包发现。
实现细节三:不回复权限请求 = 永久挂死
Kiro 执行文件写入、命令执行前会发权限请求:
json
{"id": 99, "method": "session/request_permission",
"params": {"toolCall": {"title": "Creating app.py"}}}
这是个带 id 的 request。不回复的话,Kiro 永远等着,请求就挂死了。
Headless 模式下直接自动审批:
python
def _handle_permission_request(self, msg_id, params):
self._send_response(msg_id, {"optionId": "allow_always"})
实现细节四:Chat 输出清洗
Chat 通道的 stdout 是给人看的终端输出,混着各种 UI 元素:
- ASCII 艺术 banner
- "Did you know?" 提示框
- Model/Plan 信息行
>前缀的实际回复 ← 目标内容▸ Credits: 0.09页脚
清洗步骤:
- 正则去 ANSI 转义码
- 跳过 banner 和提示框
- 找
>标记提取内容 - 遇
▸ Credits截止
这种文本解析确实脆弱,CLI 版本更新后格式可能变。但目前没有更好的办法。
实现细节五:别忘了排空 stderr
Kiro CLI 的 stderr 也有输出。如果只读 stdout 不管 stderr,缓冲区满了整个进程就阻塞。
必须开一个线程专门排空:
python
def _drain_stderr(self):
for line in self._proc.stderr:
pass # 读掉就行
这个问题在高频输出时特别容易触发。
REST API 端点一览
最终 FastAPI 服务在 8642 端口提供 7 个端点:
| 接口 | 方法 | 说明 |
|---|---|---|
/prompt |
POST | 快捷调用,支持指定模型 |
/sessions |
POST | 创建多轮会话 |
/sessions/{id}/prompt |
POST | 会话内发送任务 |
/sessions |
GET | 列出所有活跃会话 |
/sessions/{id} |
DELETE | 删除指定会话 |
/models |
GET | 列出可用模型 |
/ |
GET | 健康检查 |
接口设计很标准的 RESTful 风格。/prompt 是最常用的,大部分场景直接用这个就行。需要多轮对话时才用 sessions 相关接口。
完整请求生命周期
把上面的细节串起来,一次完整的 ACP 请求是这样的:
- 客户端发
POST /prompt - FastAPI 检测 model=auto,走 ACP 通道
- acp_client 调
session/new,拿到 session_id - 构造 JSON-RPC 请求写入 stdin
- Kiro 处理任务,通过
session/update推文本块 - 如果有敏感操作,Kiro 发权限请求,acp_client 自动审批
- Kiro 发最终响应
{"stopReason": "end_turn"} - acp_client 拼接所有文本块
- FastAPI 返回 JSON 给客户端
Chat 通道更简单:
- 客户端发
POST /prompt,指定模型 - FastAPI 检测 model 不是 auto,走 Chat 通道
- 启动子进程:
kiro-cli chat --model X --no-interactive - 等进程结束,拿到 stdout
- 清洗输出,提取有效文本
- 返回结果
调用示例
笀单演示一下怎么用:
bash
# 快速提问(走 ACP 通道)
curl -X POST http://localhost:8642/prompt \
-H "Content-Type: application/json" \
-d '{"prompt": "写一个 Python 快速排序"}'
# 指定模型(走 Chat 通道)
curl -X POST http://localhost:8642/prompt \
-H "Content-Type: application/json" \
-d '{"prompt": "优化这段 SQL", "model": "claude-sonnet-4"}'
# 多轮对话
SESSION=$(curl -s -X POST http://localhost:8642/sessions | jq -r '.session_id')
curl -X POST http://localhost:8642/sessions/$SESSION/prompt \
-d '{"prompt": "帮我写个 TODO 应用的后端"}'
curl -X POST http://localhost:8642/sessions/$SESSION/prompt \
-d '{"prompt": "加上用户认证功能"}'
局限性
坦率说几个局限:
- 无鉴权:API 没做认证,只适合内网环境,千万别暴露到公网
- 单 ACP 连接:高并发请求需要排队处理,不适合大规模并发场景
- 会话不持久化:服务重启后所有会话丢失,需要重新创建
- Chat 通道无上下文:每次都是新进程,不支持多轮对话
- 输出清洗脆弱:Chat 通道的文本解析依赖输出格式,CLI 更新可能导致解析失败
这亙限制在个人开发和小团队场景下完全可以接受。如果要上生产环境,建议加上 API 鉴权、请求队列和会话持久化。
最后说两句
把 CLI 封装成 API,技术上不复杂,难的是摸清协议的实际行为。文档没覆盖的部分,只能靠调试和抓包。
这次最大的教训:别假设协议的行为和你想的一样。 试了 8 种模型切换方式全失败,最后只能接受现实,用双通道绕过去。
完整源码已在上文逐段展示,可直接复制使用。
本文基于亚马逊云科技官方博客内容整理撰写。原文:将 Kiro CLI 封装为 REST API:双通道架构实践