发布于 更新于 约 9 分钟读完 · 3,383 字

本文目录 12 节
  1. 一句话总结
  2. 背景与事实(不可修改区)
  3. 六条可复用 insight
  4. 1. AI 只填表单,不替人做决定(核心)
  5. 2. 思考型模型的 reasoning 算在 max_tokens 里(⚠️ 首次)
  6. 3. 同一份配置被两种读法读出不同结果 = 静默失效(⚠️ 首次)
  7. 4. 探活是排在「写配置」之前的一道工序,不是之后(半开陷阱第三次现形)
  8. 5. 降级要连入口一起收走
  9. 6. 服务端读不到 git,就让已有的构建产物顺带捎一段
  10. 三条顺手教训
  11. 对外版
  12. 关联文档

任务书接 AI 辅助填写:模型只产草稿,与思考型模型的额度陷阱

入档:2026-08-03 来源:蛛网之上(tiaozhuxiansheng.com)任务书系统接入 AI 辅助填写,above-the-web 仓库 b345dfd(功能)→ 6b91311(合并上线)→ 7a64cf7(改接千问 + 专有参数口子)→ 6b12709(单引号) 状态:已上线,/api/meta 的 aiAssist 为 true,模型 qwen3.8-max(DashScope compatible-mode)

一句话总结

给既有表单接 AI,产物必须落在「人还能改」的那一格里,且服务端要把模型输出当草稿再洗一遍。 「模型说了什么」和「什么能落进表单」是两件事——前者归模型,后者归你;中间那道清洗(截长度、 日期不合法就丢、标识符重新规范化)不做,模型的每一次胡说都会变成用户的一次报错。

背景与事实(不可修改区)

  • 起点:任务书系统本身跑得通,卡在「填」。发布方发一份要写七个小节、自己把「8 月 20 号」 换算成 2026-08-20;接单的人打开认领面板是一个空的「自荐说明」框。

  • 交付:两处各加一个入口——说一句大白话,AI 填表单,人过一眼再改。另加草稿本机留存 (localStorage 的 atw-draft:*,14 天)。

  • 通道:POST /api/ai/task-draft(仅发布方)、POST /api/ai/claim-pitch(登录即可), OpenAI 兼容协议单次结构化输出调用(response_format.json_schema),零第三方依赖。

  • 验证链:服务端 node:test 50 项(新增 13,用假助手 deps.assistant 注入)→ 本机起真服务 + 假上游跑通完整链路 → 真浏览器(chrome-headless-shell + CDP,注入登录态)28 项界面断言 → 上线后拿真 key 对千问打两枪(/models 定模型 id、chat/completions 验 json_schema 与思考开关)。

  • 实测数据(qwen3.8-max,同一份「写完整八小节任务书」的请求):

    reasoning_tokenscompletion_tokens结果
    默认(开思考)235285三字段的小请求就快撑满 300 额度
    enable_thinking:false0951完整八小节、1483 字正文,4096 绰绰有余
  • 诚实边界(未验证项):

    • 没有以真实管理员身份在线上点过那两个按钮(手上没有线上 admin 口令)。线上验证只到 /api/meta 的 aiAssist:true,加上绕过应用、直接对千问的两次 curl 探针。
    • assist.js 与真千问的接缝没在同一个进程里跑过:本机全链路对的是假上游,真上游对的是 我手工拼的同形状裸 HTTP 请求。两半各自验过,接缝是推断。
    • 6b12709 那次单引号修正对生产侧的实际影响为零(systemd 本来就读对了),修的是本地 开发那条读法。写进档是因为坑本身值钱,不是因为它在线上炸过。

六条可复用 insight

1. AI 只填表单,不替人做决定(核心)

这条要落在三个具体位置上,少一个都会漏:

  • 产物落在人可编辑的控件里,不落在数据库。生成完东西还在 textarea 里,改不改、发不发 都是人点的。库里没有任何一条路径是模型直接写进去的,所以这两个接口不记流转记录、只记审计。
  • 服务端把模型输出当草稿洗一遍:标题截到字段上限、日期洗不成 YYYY-MM-DD 就丢掉(不猜)、 链接后缀按站内规则重新规范化(模型给的 " OJO Review!! " → ojo-review)、列表限长去空。 模型走的是 strict: true 的 json_schema,但 schema 只保证形状,不保证值可用。
  • 不许替人编事实。报酬、截止日期、标杆链接只有发布方知道;做过什么作品、什么时候有空 只有申请人知道。没说的一律写成「(待补:……)」占位,并在框底下列一条提醒去补。

第三条不是洁癖,是产品安全:自荐说明是发给真人看、拿来定人的。一键生成的漂亮履历, 发布方一问就穿帮,对申请人的伤害大于帮助。 连界面上的例句都跟着这条走——给的是带括号的 骨架(「做过的同类:(贴一个链接)」)而不是编好的经历,因为一键填进去就能提交的示例, 本身就是在教人注水。

2. 思考型模型的 reasoning 算在 max_tokens 里(⚠️ 首次)

千问默认开思考,而 reasoning token 和正文共用同一份 max_tokens 额度。填表这种活儿不需要长考, 不关的话额度全烧在思考上,正文反而写不完——写到一半被截断,回来是半截 JSON, JSON.parse 失败,用户看到的是一句莫名其妙的「格式不对」,会以为模型犯浑。

两条处置:

  • 关掉思考,但不硬编码。加一个 ATW_AI_EXTRA_JSON 口子,里面的东西原样并进请求体 (线上填 '{"enable_thinking":false}')。理由是「换供应商只改两个环境变量」这句话得站得住—— 思考开关、top_p 这类参数各家名字都不一样,不该为它们改代码。放在请求体最后,填了就以它为准, 这是运维手上的逃生口。
  • finish_reason === "length" 要单独给话。截断和”模型胡说”是两回事,错误信息混在一起, 排查方向就全错了。现在回的是「这次写太长了没写完,把话说短一点再试,或者手填」,日志里带 usage。

3. 同一份配置被两种读法读出不同结果 = 静默失效(⚠️ 首次)

ATW_AI_EXTRA_JSON 的值是一段 JSON,坑在外层那对单引号不能省:

读法裸写 {"enable_thinking":false}结果
systemd EnvironmentFile只在「整个值被引号包住」时才剥引号,这里原样保留✅ 对
本地 . ./.env(POSIX shell)把里面的双引号吃掉,变成 {enable_thinking:false}❌ JSON.parse 失败

最坏的地方在于失败是静默的:解析不了就退回 {}(配置写坏了不该让服务起不来),于是 思考没关掉、正文写不完,而配置文件看上去一个字都没错。加上外层单引号,两种读法才一致。

判据:配置项里放结构化值(JSON、带空格、带引号)时,外层必须自己带一层引号,并且两种读法 各验一次——tr '\0' '\n' < /proc/<pid>/environ 看进程实际拿到什么,. ./.env 看本地读出什么。 这是 账号系统对标一线_多账号抽屉与外部通道半开陷阱_v1 里「配置文件里写了什么不算数, 进程里拿到什么才算数」的续集,而且更阴——那次是发件人显示名解析不确定,这次是两条读法各自 都”正常”,只是结果不同。

4. 探活是排在「写配置」之前的一道工序,不是之后(半开陷阱第三次现形)

拿到 key 的第一反应是按惯性配成 api.ofox.ai(站里 api-proxy 一直在用这家)。重启, /meta 的 aiAssist 老老实实变成 true——功能看着是开了,可上游一调就是 401 Invalid or expired API key。那把 key 其实是千问的。

这正是 账号系统对标一线_多账号抽屉与外部通道半开陷阱_v1 那条律的第三次现形: 页面说功能开着、真调却必失败,比没有这个功能更糟。 所以发现 401 之后第一件事是把 key 注掉、 让它退回隐身,再去查——而不是留着一个点了必挂的按钮边查边挂。

正确顺序(已写进 .env.example 的注释——把顺序写在配置文件旁边,比写在事后复盘里管用):

  1. GET /models 探这把 key 能用哪些模型。免费,而且能拿到准确的模型 id(这次就是靠它 把「千问 3.8 Max」定成 qwen3.8-max,同时确认了 key 归属);
  2. 打一次真实的结构化输出调用,看 usage 和 finish_reason——支不支持 json_schema、 思考烧多少 token,都在这一枪里;
  3. 最后才写进 .env 重启。

与 OpenAI兼容止于对话端点_多提供商视频接口分流与真key首测_v1 的「真 key 首测是一道独立工序」 是同一条纪律,本次补的是它在时间轴上的位置:首测要在配置生效之前,否则「首测」变成 「线上事故的第一现场」。

5. 降级要连入口一起收走

没配 ATW_AI_API_KEY 时,/meta 的 aiAssist 为 false,前端连按钮都不渲染, 页面上看不出少了什么,手填一路照旧;模型那头出岔子回 502 加一句「手填也能发」, 对方的原话只进日志(里面可能带用量、账号信息)。

「按钮在、点了报错」和「按钮压根不出现」,对用户是两种体验:前者是坏掉的功能,后者是没有这个功能。 同族见 reduced-motion本机陷阱_动效降级纯淡入而非跳过_v1——降级要退到一个完整可用的形态, 不是把路留在那儿让人撞。

6. 服务端读不到 git,就让已有的构建产物顺带捎一段

写自荐说明得知道这活儿要干什么,可 md 任务书的正文在 git 里,平台服务读不到(库里的 body 只有站内新建那批才有)。没有为此新开同步通道,而是让本来就存在的对接产物 /tasks/index.json 多带一个字段 outline(正文纯文本节选,去掉代码块图片链接语法、截到 2400 字), 同步时写进 tasks.outline。

三条约束跟着落位:只读不改、不进任何公开接口、清单里没有这个字段(老版本构建产物)就不动 库里已有的那份,不会把它抹成空。这是 静态站接账号系统_内容归git状态归库的双源切分_v1 那条双源切分的直接复用——要跨源传数据时,先看现有的那条缝够不够宽,别急着开第二条。

三条顺手教训

  • 隐私边界要有一条断言看着它。 送进 prompt 的只有任务书本身、用户的公开资料(昵称 / 简介) 和他自己写的那段话;联系方式、收款方式、邮箱一概不带。这条写在注释里会腐烂,所以 test/assist.test.js 里有一条断言直接查 prompt 里不含那几个值。
  • 登录后的页面用 --dump-dom 验不了,令牌在 localStorage,一次性 dump 没有注入时机。 改用 CDP:同一个 chrome-headless-shell 加 --remote-debugging-port,Node 22+ 有全局 WebSocket,零依赖连上 /json/new 拿到的 webSocketDebuggerUrl,就能写 localStorage、 点按钮、读 DOM、截图。补 成稿链接存进库没人读_写入方不等于展示方_v1 里那套 --dump-dom 姿势够不着的那一半。
  • 截图验昼夜别用 Emulation.setEmulatedMedia。 本站主题看 document.documentElement 的 data-theme(localStorage 的 atw-theme),不看 prefers-color-scheme——设了媒体特性, 截出来两张一模一样,会以为暗色皮肤没做。直接 setAttribute('data-theme','dark')。 同族见 reduced-motion本机陷阱_动效降级纯淡入而非跳过_v1:本机/无头环境的媒体特性, 和产品真正的开关未必是同一个。

对外版

08_对外分发/任务书AI辅助填写_粉丝群公告.png(长图 2240×4250,附 .html 渲染源码)—— 发粉丝群的更新公告,走 07_skill存档/report-longimage 出图。对外版把 insight 1 的 「不替你编经历」放进了全篇唯一的红线块,并如实写明当时 0 个在招任务、不诱导空点。

关联文档

类型/平台工程主题/AI接入来源/蛛网之上