一个月从零到生产:我做了一套对接巨量引擎的千川 AI 投流系统,这些坑都是真金白银换来的

4842 字
24 分钟
一个月从零到生产:我做了一套对接巨量引擎的千川 AI 投流系统,这些坑都是真金白银换来的

9 月 8 日下午我在 package-lock.json 里敲下第一行依赖,10 月 7 日上午最后一次构建产物落进 dist/。整整一个月,一套对接巨量引擎(千川)开放平台的投放管理与调控系统,从空目录跑到了生产环境,服务真实的投放团队。这篇文章是完整复盘:为什么做、怎么搭、踩了哪些坑、AI 调价引擎为什么这么设计。

一、为什么做这个系统#

我们是有直播电商投放团队的品牌方。投手的日常是这样的:几十个千川广告主账户散落在后台,看数据要挨个切;一场直播爆了,好素材要手动建追投任务;ROI 目标靠人在大屏前盯,每十五分钟看一眼消耗和成交,高了调低、低了调高;评论区的问题分散在各个抖音号下面,想按号归拢只能靠平台后台慢慢翻。

每一件事单看都不难,难的是「持续、及时、不出错」。人盯盘有两个致命问题:一是半夜盯不动,二是情绪化——数据一抖就手抖。所以我决定做一套系统把这些流程接进来:多账户 OAuth 授权管理、计划总览、素材追投、直播大屏、评价(评论)管理,最后加一个 AI 调价引擎,让「加多少减多少」这件事交给数据和模型判断。

二、系统全景#

上线后的形态(nginx + systemd 部署在一台云服务器上,域名就不公开了):

浏览器(React 19 + Vite 8 单页应用,dist/ 静态产物)
│ /api/* 反向代理
▼
Node 服务(127.0.0.1:8787,纯 Node 内置模块,无 Express)
│ Access-Token 鉴权 + 频控退避
▼
巨量引擎开放平台(OAuth / 全域计划 / 调控任务 / 素材 / 评论 / 直播大屏 / 报表)

几个关键的技术选型说清楚:

  • 后端零依赖。server/index.mjs 用 node:http 手写路由,一共几十个 API 路由,好处是生产服务器上连 npm install 都不用跑,rsync 上去重启 systemd 就完事。平台客户端独立在 server/oceanengine.mjs,AI 顾问在 server/ai-advisor.mjs。
  • 前端不引组件库。React 19 + Vite 8,图标用 lucide-react,样式全部手写。整个前端几乎是两个大文件:src/main.jsx(约 54 万字符)和 src/styles.css。不体面,但一个月工期里这是最快收敛的方式。
  • 共享契约层 shared/。调价决策核 bid-engine.mjs 是纯函数、无 IO、无依赖,Node 内置 test runner 直接跑 48 个用例。
  • 数据存储是 JSON 文件(server/data/ 下的 oauth-grants.json、plans.json、audit-events.json、bid-bindings.json 等)。没有数据库。对一个单团队内部系统,文件 + 原子写 + 启动恢复够用,而且备份就是 cp。
  • 测试是验收货币:npm run test:contracts(48 项契约测试)+ smoke + domain + build,后来又加了 48 项引擎纯函数测试和 21 项引擎服务端回环测试(用本地 mock 平台 scripts/local-e2e-mock.mjs 挡住真实外呼)。

功能模块(侧栏即产品):账户授权管理、账户中控台、全域计划总览(行内补预算/调 ROI)、素材调控管理(素材列表 + 追投 + 调控任务)、批量创建计划(本地草稿)、AI 直播调价策略、评价管理、操作日志、系统管理(真实写入总开关、AI 模型配置)。

安全边界是第一天就定下的(项目根 AGENTS.md):默认全只读;真实写操作(补预算/调 ROI/追投/回复评论)必须过「总开关 + 稳定幂等键 + 写入台账(SUBMITTING→RESPONDED 状态机)+ 审计 + 提交后自动回读核验」五道门;删除类平台操作在任何情况下拒绝(调控任务状态接口的 DELETE 枚举在服务端直接拦掉)。

三、开发周期复盘#

9 月上旬(9-08 ~ 9-13):文档先行 + 对齐操作习惯。 第一天先写的不是代码,是 docs/PRODUCT_SPEC.md、DEVELOPMENT_PLAN.md、REUSE_STRATEGY.md。9-11 组装了 docs/ad-system/ 文档包:48 项功能逐项规格、48 个候选接口映射、64 条验收用例、21 个待核验问题。接口映射表里每个接口都标了核验深度(ENDPOINT_ONLY / PARAMETER_PARTIAL / SDK_VERIFIED),「接口存在」不等于「权限可用」,这句话后来救了我很多次。9-12~9-13 对照在用的老系统(飞流智投)逐页对齐交互——计划总览的筛选结构、27 列宽表、「+/准星」行内按钮的语义,都用无头浏览器截图核过。9-13 打通真实 OAuth:授权回调返回 ok,工作台账户接口展开出 68 个千川业务账户,uni_promotion/list/ 读到第一条真实直播全域计划。

9 月中旬(9-14 ~ 9-18):读取链路工程化。 68 个账户 × 4 个场景 = 272 次只读请求,最初在浏览器里串行发,频繁撞 40100 频控。9-15 写了 docs/PLAN_READ_DESIGN.md,把读取搬到服务端持久化任务队列(plan-read-jobs.json):全局限速、指数退避(60s/120s/300s)、断点恢复、重启不重读。9-16 动策略模块前先做了全量备份(_backup_pre_fix_20260916/,连 server/data/*.json 都记了 MD5),9-17 调整环境配置(留了 .env.bak-20260917163059),9-18 契约测试扩到 48 项全绿。

9 月下旬(9-19 ~ 9-23):写链路与素材/评论模块。 9-19 接操作员登录(operator-auth.mjs,公网部署关闭自助注册)和评论只读接口;9-22 素材调控模块重构,重构前又做了一次带 SHA256SUMS 的全量备份(_backup_material_module_20260922_143636/,含恢复演练步骤);同一天对着官方 Go SDK 逐字段核验素材/调控任务契约,9-23 直接在千川后台实测追投弹窗,拼出「视频追投与直播间画面追投是同一接口的两种对象」这个关键认知。

10 月上旬(10-03 ~ 10-07):AI 调价引擎与评价管理冲刺。 10-03 核验直播大屏三件套接口并上线大屏视图;10-04 写部署文档、AI 评审接入做成页面在线配置;10-05 定下「规则只做约束、判断归 AI」的架构(这天下深夜还出过一次误删文件事故,后面细说);10-06 白天给 AI 调价加「数据门」修振荡、完成 ROI 口径代数实证,晚上评价管理模块部署生产、真数据自验通过;10-07 上午还在迭代前端。

四、踩坑实录:每一条都有代码佐证#

1. 追投接口的实测契约——平台的回显不能照抄进创建请求#

素材追投走 uni_promotion/ad/control_task/create/。这个接口我前后失败了 21 次才拼出完整契约(台账里 21 次创建失败全是参数问题):

  • 预算:必须是 100 的整数倍,且不超过原计划预算的 50%(2026-09-24 实测定论,前端校验文案里就写着这两条)。
  • duration(控成本追投):任务回显里 duration 是 87672(约十年,等于「长期有效」),但你创建时传 87672 会被 schema 拒(“must be most 24”),传自定义值又被语义拒(「不支持自定义时长」)——正确做法是完全不传,平台自动落默认。放量追投才传 0.5~24 小时、0.5 步进。
  • audience(定向人群):回显是全 null 的完整对象,但创建时数组型键不接受 null(平台报 “Value is not nullable”)——正确做法是把值为 null 的键整个去掉,没设置定向就传空对象 {}。读取侧的形态不能照抄进写入侧,这是这次学到的最反直觉的一课。
  • 400154 排查:平台只回笼统的 400154「请求参数设置错误」,不指名字段。我的办法是给每个写入请求落一份截断的请求体快照进台账,事后能对照;后来定位到一次 400154 的真实原因是把直播间画面素材 ID 塞进了 material_ids 却没带 material_type,平台按视频素材查这个 ID 查不到。画面追投要走独立的 room_material_id 字段,单一固定目标,不能和视频混投。
  • SYSTEM_DISABLE 是常态:全域直播计划未开播时常驻「系统暂停」状态。早期我把它当「平台可能拒单」的预警条件,属于错误归因——修正参数后,同状态计划连续 12 次创建成功。所以现在只对真正的终态(已删除/已冻结/余额、预算下线)提示,常规暂停态不拦。
  • 19 位 ID 精度:素材/任务/评论 ID 是 19 位整数,超出 JS 安全整数范围,Number() 一转末位就舍入——追投会打到另一个素材上。读路径在 JSON.parse 前对长整数做正则引号化保真;写路径用哨兵法:序列化时先包 @@id@@ 再正则还原成裸数字。

2. 直播大屏接口:fields 必填、时间必须带秒#

today_live/room/get 必须带 advertiser_id + aweme_id + date_time(北京日期),fields 是 JSON 数组查询参数;today_live/room/product_list/get 的 fields 同样必填且 productId 是驼峰;report/live/get 的 start_time/end_time 必须是 yyyy-MM-dd HH:mm:ss 带秒格式。少了 fields 或只传到日期位,接口直接报参数错误。这些是 10-03 拿线上凭证逐个直调核验后写进 oceanengine.mjs 注释的。

3. 考核 ROI 的口径是算出来的,不是读出来的#

页面上有好几个 ROI 字段,到底哪个和 roi2_goal 同口径?我用一个生产计划做了代数实证:平台返回考核 ROI 2.06,当日含券成交 4881、消耗 2367.49,4881 ÷ 2367.49 = 2.062,吻合;换实付口径算是 1.377,不吻合;stat_cost_for_overall_roi2 字段恒为 0,不可用。结论:考核 ROI = 含券 GMV ÷ stat_cost(对应平台字段 total_prepay_and_pay_order_roi2,分子用 total_pay_order_gmv_include_coupon_for_roi2)。这条口径后来直接写进了 AI 提示词和引擎数据门,是整个调价引擎的度量基石。

4. 评论归属:反查接口有上限,第 11 个抖音号被截掉了#

评价管理要做「每条评论归属到哪个授权抖音号」。平台评论接口 tools/comment/get 没有「视频归属抖音号」字段;aweme/video/get 和 tools/aweme_auth_list/ 实测都返回 40002「advertiser does not grant you permission」,此路不通。实测可用的路径是:tools/comment/get 的 filtering={"author_ids":[数字uid]} 支持按视频作者抖音 id 过滤,且账号间评论天然隔离——某账号能扫到的 item_id 就归属该账号。于是反查变成「逐授权号带 author_ids 扫评论建映射」。

两个细节坑:一是 author_ids 必须传数字 uid,超出 JS 安全整数范围的 uid 我宁可 422 拒绝(错误码 AWEME_ID_INVALID)也不能四舍五入后扫错账号——那等于把归属安到别人头上,是伪造数据;二是反查有预算上限,最初只扫前 10 个授权号,10-06 生产上授权号涨到 11 个,第 11 个号(某位主播)的评论归属恒为空,用户报「抖音号·本账号列全是空的」。根因就是这个 10 的上限把人截掉了。修复:上限放宽到 20、时间预算放宽到 20 秒、逐号 250ms 错峰。当晚生产真数据自验:20 个授权号、20/20 条评论全部归属成功。另外审计日志的实现是新记录 unshift 插在数组头,最新事件永远在第一位,和界面上「最新在前」的直觉一致。

5. OAuth token 自动刷新#

access_token 有效期 24 小时,不能让人每天重新授权。实现是 ensureAccessToken:过期前 5 分钟自动轮换;用 Promise 单飞防止并发请求同时刷;平台轮换 refresh_token 后立即把新 token 对落盘回 grants 文件,否则下次重启就丢刷新链;重启恢复时 accessToken 为空,按用过的 refresh_token 反查同一条授权补写。服务重启不掉线,这条在生产跑了三周没出过事。

6. 部署工程化:rsync 要护住 data/#

部署流程写在 DEPLOY_SERVER.md:本地 VITE_API_BASE=<生产域名> npm run build,前端产物先 rsync 到服务器 /tmp/dist-new/ 暂存,再 sudo rsync 进 nginx 静态目录并 chown www-data;后端只同步 server/ shared/,必须加 --exclude data/——server/data/ 里有线上真实授权凭证与快照,一次手滑同步就等于覆盖生产授权。动任何东西之前先做带校验和的全量备份(9-16 那次连数据文件记的是 MD5,9-22 那次是 SHA256SUMS,都留了恢复演练命令)。CORS 默认只放行 127.0.0.1:5173/4173 这几个开发端口,线上站点通过 ALLOWED_ORIGIN 显式指定。还有一个反直觉的点:systemd 服务不能设 NODE_ENV=production——代码里的租户熔断会在这种状态下把所有业务接口 503 掉,线上安全靠的是登录门禁 + 关闭自助注册,和本地同构。

7. AI 调价引擎:建议振荡与数据门#

这个单独立一节讲,因为它是整个系统最有意思的部分。

五、AI 调价引擎:「规则只做约束、判断归 AI」#

架构决策的来龙去脉#

最早的引擎是纯规则的:实际 ROI 高于目标就降标放量、低于目标就升标收紧,固定 15% 步长。跑起来发现规则引擎有个天花板——它只会看「偏差方向」,不会看「量级和节奏」。偏差 12% 和偏差 40% 用同一个步长,消耗在萎缩和消耗在爆发用同一个动作,这不叫调控,叫条件反射。

10-05 深夜我定下架构:规则只做约束,判断归 AI。规则层保留全部硬约束——目标上下限、单次幅度、15 分钟节流、每日 8 次上限、日累计幅度 40% 封顶、观察期、熔断;「加多少减多少」由 AI 每轮拿整场直播数据判断,输出 {action, suggestedRoiTarget, risk, note} 的严格 JSON。AI 的提议回来还要再过一遍服务端硬约束(clamp 到上下限、升标 ≤5%、降标 ≤20%,超限自动收敛不拒绝)。

配套的安全策略是:AI 欠费、超时或未配置时,不产出任何 RAISE/LOWER 建议——引擎的跳过计数器记 SKIP_AI_NOT_CONFIGURED,数据照读、审计照记、熔断保护(PAUSE_SUGGEST)照常,但宁可不动也不乱动。规则引擎的原始决策不再作为建议输出(启动时还会清理历史纯规则遗留建议)。唯一例外是熔断:连续烧钱零成交(自上次出单花费 ≥ 3 × 推导成交成本)时的暂停建议保留,因为这是保护性止血,不是调价判断。

数据门:修掉建议振荡#

AI 上线第一晚就暴露新问题:小样本窗口的 ROI 剧烈虚高——一个 15 分钟窗口只有 1 单、几块钱消耗,ROI 能飙到 15+,AI 一看「远超目标」就建议降标,下一个窗口数据回落又建议升标,两小时 5 次反转。

10-06 加了两道数据充分性硬约束(在 mergeAiAdvice 里,AI 提议也必须过):

  • 门 D1 累计死区:主口径改为「当日累计考核 ROI」(优先平台返回值),距目标偏差不足 ±10% 直接 HOLD——短窗口波动不构成调价理由;
  • 门 D2 降标样本门:LOWER(降标放量)要求最近窗口成交 ≥3 单,样本不足不许降。

同时给 AI 的上下文做了两件事:一是所有比率引擎先算好,不让模型自己做除法(它会算错,而且不可复现);二是口径写进字段名——当日累计_考核口径、前15分钟窗口_引擎估算、本场直播实时_大屏支付口径,提示词里明确「窗口成交单数 <3 时不得作为下调目标的依据」「大屏支付口径天然低于考核口径,仅作参照」。修完之后振荡消失,建议节奏稳定在「调一次→观察四个窗口→再评估」。

两个生产细节#

模型延迟:生产接的是火山方舟 doubao-seed 系列,默认开深度思考,一次判断 25 秒起步,超出引擎等待窗口。实测给请求体加 thinking:{type:"disabled"} 后 1~2 秒返回且质量不受影响——现在代码里对 doubao-seed 前缀的模型自动带这个参数。Key 存 ai-config.json(0600 权限,页面只回显掩码),页面保存即时生效不用重启。

误删事故与恢复:10-05 定架构那晚,我在重构引擎代码时误删了文件——好在编辑器的文件状态快照(readFileState)里存着被删文件的内容,从快照里把代码恢复了出来,损失了半小时而不是一晚上。这次之后我给「大改先全量备份」这条纪律又加了一层保险,也顺手把备份目录的恢复演练写成了文档。

交互形态#

绑定流程做成向导(账户→计划→参数),绑定后进入计划列表页(状态筛选 + 搜索 + 详情钻取),每个计划有独立详情页:建议卡显示当前目标/建议目标/目标区间/AI 判断与理由(AI 原提议和硬约束收敛后的采纳值不一致时会标注「已按上下限/幅度约束收敛」)。计划列表专门加了「实际 ROI」列,展示的就是第三节实证出来的考核口径,旁边才是出价目标 ROI——投手一眼能分清「跑出来的」和「设进去的」。

六、效果#

一个月后回头看交付了什么:

  • 一个生产系统:多账户 OAuth(token 自动轮换不掉线)、全域计划总览(行内补预算/调 ROI 真实写)、素材追投(幂等 + 台账 + 回读核验)、直播大屏、批量创建(本地草稿),前端 48 项 + 引擎 48 + 21 项测试全绿。
  • 评价管理 10-06 上线当晚真数据自验通过,20 个授权号评论 20/20 全部归属成功。
  • AI 调价引擎在「判断归 AI + 数据门」架构下振荡消失,危险动作层层设卡:AI 高危否决(AI_VETOED_HIGH_RISK)不自动执行、写入后回读不匹配自动冻结该计划、调价后连续 3 轮 ROI 偏差恶化自动暂停当日执行。
  • 最值钱的资产其实是那套契约知识:48 个接口的核验深度标注、追投创建的「回显镜像」契约、ROI 口径实证——这些是用真实的 21 次失败换来的,全部固化在代码注释和 docs/ 里。

七、反思与后续#

做对了的:文档先行、核验深度分级(「接口存在」永远不等于「权限可用」)、默认只读 + 写入门禁链、大改前全量备份、决策核纯函数化。这五条让我在一个月里没有出过一次真实资金事故。

没做好的:单体文件太大了,index.mjs 43 万字符、main.jsx 54 万字符,接下来的第一件事就是按模块拆分;JSON 文件存储在多实例场景会到瓶颈,迁移 PostgreSQL 的设计文档其实早就写了,一直没排上;追投编辑接口对 duration 的语义(update 支持 1.5~24 小时)和 create 不同,这块的用户提示还不够直白。

后续方向:把 AI 判断的离线回测跑起来,用历史直播数据对比「AI 调价 vs 人工调价」的 GMV 和利润差;素材侧接上高潜识别,让追投建议也从「人挑」变成「数据挑」;再往后是托管策略的完整闭环——但每一步都会继续守着那条红线:宁可不动,也不乱动。

文章分享

如果这篇文章对你有帮助,欢迎分享给更多人!

一个月从零到生产:我做了一套对接巨量引擎的千川 AI 投流系统,这些坑都是真金白银换来的
https://liqiming.top/posts/qianchuan-ai-ad-system/
作者
图明
发布于
2026-10-07
许可协议
CC BY-NC-SA 4.0
相关文章智能推荐
1
从 SSG 到全栈:我给博客造了一个管理后台
技术实践Astro SSG + Hono + SQLite + Artalk 评论对接 + 自建访客统计 + 自动发布管线:一个人把静态博客做成全栈 CMS 的完整技术方案(修订版)。
2
我给大肥鱼装了一整套人生:一只开源 QQ 机器人的完整部署实录
技术实践从一条贱兮兮的鲸鱼娘提示词,到会在千人群里当群友、会发空间、会萝莉音说话、知道自己脑子开源在哪的 QQ 机器人——AstrBot + SnowLuma 全栈部署,踩了 23 个坑,全部开源。
3
十一个星期从零到 223MB Windows 便携包:我做了一套 AI 种草 Live 素材生成工具,让视频模型只出无字底片是最省钱的设计
技术实践复盘 11 周开发周期:四大 Provider 抽象、素材库、小红书爆款采集、RAG 洗稿、真人手写风格、免登录自配便携版,以及图片 200 假成功、70 个文件被外部回退、macOS 交叉打包 Windows 这些真金白银换来的坑。
4
一次真实的 Redis 入侵:我的服务器是怎么被打进来的,以及我是怎么修的
技术实践弱密码 + 公网端口,两天内被两个 IP 光顾:一个清了我的库,一个把数据同步到了他的服务器。完整复盘发现、取证、修复全过程。
5
2026 年 10 月 AI 圈:Gemini 4 Argon 掀桌,OpenAI 卖「AI 员工」,Claude 开始参与写 Claude
AI 资讯国庆前后这一周多,谷歌、OpenAI、Anthropic 接连出牌:Gemini 4 Argon 冲击真实软件工程纪录,DevDay 把 ChatGPT 改造成智能体平台,Claude 已主导自家 26% 的模型研发;国内云栖大会亮出算力三倍的真武 V900。
随机文章随机推荐

评论区

Profile Image of the Author
图明
记录 AI 资讯、技术实践与持续学习。
公告
欢迎来到启明的博客:这里持续更新 AI 资讯、技术解读与工程实践。还有我的日常哦
分类
标签
热门文章
站点统计
文章
13
分类
8
标签
43
总字数
28,868
运行时长
0 天
最后活动
0 天前
站点信息
构建平台
Local
文章许可
CC BY-NC-SA 4.0
文章目录