AI 生成机器人暴露限流与响应解析漏洞
作者重建 Grok 辅助生成的 Kalshi 模拟交易机器人,发现其忽略 Retry-After,并将登录页面当作 JSON 处理。测试仅使用公开演示数据,未发送订单,揭示正常流程运行成功仍会遗漏的 HTTP 边界问题。
AI 深度解读
Grok 能不能写交易机器人?这次复建给出的答案很具体:能搭出一个读取公开行情、在本地模拟成交的程序,但生成代码的可靠性,要靠故障测试来验。作者发现,它会无视服务器要求的等待时间,还会把登录页当成 JSON 解析。
这篇最值得程序员看的,是一次 AI 编程交付验收:哪些约束必须写进代码,哪些正常运行发现不了的问题要主动注入,以及「测试通过」究竟能证明到哪一步。
AI 写代码,限额和开关由谁负责
作者于 2026 年 11 月 5 日上午(美东时间),在干净目录里使用已发布的一次性提示词,手动完成构建,并连接 Kalshi 的公开 demo 行情运行。默认开启 paper mode,即读取真实接口返回的数据,在本地模拟订单和成交。
公开读取使用 https://external-api.demo.kalshi.co/trade-api/v2,不需要密钥。发送 demo 订单则要求密钥、PAPER_MODE=0、--live 和手动输入确认。作者没有 demo 密钥,因此没有发送签名请求,也没有提交订单。
这里的设计重点是:支出限制放在 limits.json 和 risk.py,不只写在给 AI 的提示词里。模型负责生成代码,运行时由代码检查是否允许继续。
提示词表达要求,运行时检查才执行约束。
可选的研究步骤也被限制了权限:只把公开的市场标题、规则文本和价格发送到 xAI 的 Chat Completions endpoint,返回结果只记日志,不修改报价。这次 research_enabled false,该步骤没有调用。因此,文章验证的是程序链路,没有验证 Grok 的研究质量。
订单接口也有明确边界。Create Order V2 使用 POST /portfolio/events/orders,参数包含 side、count、price、time_in_force 和 self_trade_prevention_type。其中 time_in_force 支持 fill_or_kill、good_till_canceled、immediate_or_cancel;批量撤单使用 DELETE /portfolio/events/orders。
为什么一次 429 比正常成交更能暴露问题
最先暴露的问题是限流:服务器返回 429,并带上 Retry-After: 3,旧客户端却在约 0.5 s 后重试。它虽然写了退避逻辑,却没有遵守服务端明确给出的等待时间。
修复后,有 Retry-After 就按它等待,上限 8 s;没有这个头时,对 429/5xx 使用有界指数退避。公开 GET 配置为 timeout=(5, 10),最多重试 2 次。这里两个超时值分别约束连接和读取,不能直接当成整个操作的总时限。
在只返回一次 429 的测试里,客户端等待约 3 s,总耗时 3.84 s。旧版持续遇到 429 时,五次长重试耗费约 22.6 s;修复后的持续 429 测试在 1.51 s 失败。不过,作者没有交代后一组响应是否携带同样的等待头,不能把它和前一组直接当成同条件提速对照。
另外两类故障更贴近日常接口开发:
- 网关把登录页作为 HTTP 200 返回,旧版直接抛出
Expecting value: line 1 column 1。修复后输出fetch failed: response was not JSON: gateway login。 - 503 返回 HTML 时,旧版把标签带进错误信息,修复后压缩为
503 Service Unavailable。
无响应主机则暴露了等待预算的问题。旧版被外层进程等到 55 s 后终止,仍没有有用输出;修复后在该次测试中约 16.5 s 输出 fetch failed: timed out。请求期间仍不能保证 Ctrl-C 立即响应,作者明确保留了这个限制。
原文给旧项目的升级提示词中,这几条可以直接用于 HTTP 客户端验收:
1. Public GETs: timeout=(5, 10), at most 2 retries. On 429/5xx use
bounded exponential backoff. If the response has Retry-After, sleep
that many seconds instead (cap 8).
2. Never print a traceback for HTTP or JSON failures. One line only:
"fetch failed: …". HTML or non-JSON 200 -> "response was not JSON: …"
(strip tags, truncate). 503 HTML -> "503 Service Unavailable".
3. Hang/timeout -> "fetch failed: timed out" after the retries, then exit.
4. Catch BrokenPipeError around main (exit 0) so `| head` is quiet.
KeyboardInterrupt -> one line, exit 130.
要看的是第一条:重试次数、单次超时和服务端等待要求必须一起检查。第四条则补上了命令行使用体验,管道提前关闭和用户中断都有明确退出方式。
模拟成交正确,还要证明账没有算错
11 月 5 日的复建保住了此前 10 月 25 日修正的几项行为。它们看起来像记账细节,却直接决定限额有没有作用。
订单组限制的是滚动 15 s 内已撮合的合约数量。如果每次运行都新建订单组,就无法用同一个组累计限制。作者改成创建后复用;但服务端是否真的拒绝超限订单,这次没有密钥,仍未验证。
模拟成交也不能按限价直接记账。一次 IOC(immediate-or-cancel,立即成交否则取消)买单,限价为 0.1500,实际卖价为 0.0600,本地模拟应按后者成交。日志记录了 1 份合约、$0.01 手续费,以及 $0.07 的风险敞口。再次运行遇到已持有标的,会跳过购买。
开启 KILL 后,程序拒绝下单,也不创建订单组。执行 cancel_all 后,paper_cancelled_resting: 0,已经成交的持仓和敞口继续保留。撤掉未成交订单,不能把已经承担的风险一并抹掉。
每日亏损则先结算再检查。作者注入了一个测试持仓,对应 demo 市场已经最终判定为 no,程序记录 pnl -0.13,计入当日亏损。随后第二个标的按 0.0100 模拟成交,复用 paper-6cfc6744 订单组,总敞口达到 $0.09。这是结算逻辑演练,不是实际交易业绩。
手续费按 0.07 * count * price * (1 - price) 计算,并向上取整到美分。不过,作者检查时费率页面返回了 429,只确认模拟程序沿用了提示词中的公式,没有完成当次官方费率核对。
行情本身也会误导初学者。yes_ask_dollars 为 "0.0000" 或 "1.0000" 都按没有有效卖价处理;从只含买价的订单簿推导 YES 卖价时,应使用 $1.00 减去最高 NO 买价,不能对美元价格计算 100 − X。
当天约 8:57,使用 mve_filter=exclude 扫描 5,000 个开放市场,只找到 56 个有效 YES 卖价,其中 18 个不高于 $0.15。约 9:04,前 1,000 个市场只有 13 个有效卖价,符合该价格上限的为零。两次扫描范围不同,不能据此精确衡量流动性下降,但足以说明运行前应重新选择标的。
接口变化也会影响判断:作者引用的 11 月 5 日变更中,未限定范围的 GET /markets 排除了按需生成的 KXRFQ 系列;停牌或关闭市场的订单错误由 market_closed 改为 market_inactive。生命周期状态 active / finalized 没有改,程序仍只在 active 时出价。
我的判断
这篇最有价值的地方,是作者把验证范围写清楚了:16 项离线测试通过,加上公开 demo 行情和本地代理故障注入,支持的是重试、超时、模拟成交、记账与开关逻辑。
它没有证明交易策略有效,也没有证明真实订单链路可靠。
真实 demo 成交、订单组拒绝、签名撤单都没测,Grok 研究步骤也没调用。标题中的 Grok 不是这些结果成立的关键因素,更不能由此比较 Grok、Claude 和 ChatGPT 谁更擅长写机器人。
把限额放进代码,比只靠自然语言约束更可检查;但「模型无法跳过自己不控制的检查」有一个前提:执行路径必须经过检查,而且检查实现正确。如果代理仍能修改这些文件,单独存在一个 risk.py 并不等于形成独立权限边界。
超时和重试参数同样只是这次演练中的有效配置。作者没有给不同网络环境的对照数据,普通团队应按自己的请求预算调整。相较于继续润色提示词,这篇更有说服力的做法,是把故障变成可重复输入,再检查输出和状态。
能拿来做什么
- 给 AI 生成的前端请求封装或 Node 客户端补一组故障验收:429 携带
Retry-After、200 返回登录页、503 返回 HTML,以及请求迟迟不返回,分别检查等待、报错和退出行为。 - 对会修改状态的 AI 编程项目,把限额、重复操作检查和停止开关写进执行路径,并验证停止后不会继续创建任务或资源。
- 验收模拟环境时,同时检查成交价格、手续费、持仓和撤销后的状态。模拟运行通过后,仍把签名请求与服务端拒绝列为独立待验证项。
- 让编码代理提交测试结果时,附上数据来源、故障注入方法和未覆盖范围,避免一句「已验证」掩盖实际只跑过正常路径。