news 2026/10/14 16:38:34

Go 后端转 AI:AI 功能怎么报价?一张 10 行自检表说清口径

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Go 后端转 AI:AI 功能怎么报价?一张 10 行自检表说清口径

上周有个人问我:我们这个 AI 客服上线一个月,大概要花多少钱。

我说你先告诉我三个数:一次对话平均调几次模型、每次输入多少 token、重试率多少。

他一个都答不上来。

这在我这儿不是第一次。能报出模型单价的人十有八九,能说出自己那个功能单次成本的人几乎没有。到要报价的时候,只能拍一个数,拍低了亏自己,拍高了单子飞了。

这篇文章把成本这件事拆成可算的口径。看完你应该能自己填出一张表。

一、先把「一次业务请求」和「一次模型调用」分开

成本算不清,八成是因为这两件事被当成了一件事。

用户点一次「发送」,这是一次业务请求。但这个请求背后可能调了模型好几次:

  • 意图识别调一次(通常是小模型);
  • 检索到的片段要不要用,可能再调一次;
  • 生成最终回复调一次(通常是大模型);
  • 如果第一次超时重试,上面每一步都可能再走一遍。

也就是说,一次业务请求的实际花费,是这条链路上所有调用的花费之和,还要乘上一个重试放大系数。

我在项目里见过的最夸张的情况:一个看起来很简单的问答,因为工具调用老是抖动,重试放大到了 2.7 倍。预算是按 1 倍算的,上线第一周就超了。

二、token 计费不是一个数,是三段

模型厂商的报价单上,通常至少有三行:

费用项计价单位谁在放大它常见漏算怎么测降本手段
输入 token每百万检索片段长度、历史对话轮数把 system 提示词忘了算服务端在调用前打印 prompt 长度缩短提示词、裁剪历史
输出 token每百万模型话多、格式没约束按平均值估,实际长尾很贵服务端累加 completion 长度限制最大输出、要求结构化
缓存命中输入每百万(打折)提示词前缀是否稳定前缀里塞了时间戳,缓存全废命中率从返回体里读固定前缀顺序、不放动态值

第三行是很多人没注意的省钱项:如果你的提示词前缀是稳定的,命中缓存的那部分输入会便宜很多。但只要你把当前时间或者随机数放进前缀,命中率立刻掉到接近零。

这是个很典型的后端能发挥的地方。它不需要你懂模型,需要你懂「什么东西会被当成 key」。

三、用一段 Go 把成本累加起来

讲完口径,落到一个能直接用的东西。下面这段是我自己在服务里用的做法:每次调用完打一条记录,按请求 ID 汇总。

packagecostimport("fmt""sort""sync""time")// Price 是每百万 token 的单价,单位:元。typePricestruct{InputPerMfloat64OutputPerMfloat64CachedInputPerMfloat64}// Record 是一次模型调用的记录。RequestID 用于把同一次业务请求// 的多次调用串起来,Attempt 记录它是第几次尝试。typeRecordstruct{RequestIDstringModelstringInputTokintOutputTokintCachedTokintAttemptintAt time.Time}typeTrackerstruct{mu sync.Mutex pricesmap[string]Price records[]Record}funcNewTracker(pricesmap[string]Price)*Tracker{return&Tracker{prices:prices}}// Add 拒绝两类坏数据:没有登记单价的模型、以及 Attempt 小于 1 的记录。// 这两类一旦放进去,后面算出来的成本会静默偏小,比报错难查得多。func(t*Tracker)Add(r Record)error{t.mu.Lock()defert.mu.Unlock()if_,ok:=t.prices[r.Model];!ok{returnfmt.Errorf("no price registered for model %q",r.Model)}ifr.Attempt<1{returnfmt.Errorf("invalid attempt %d for request %q",r.Attempt,r.RequestID)}t.records=append(t.records,r)returnnil}func(t*Tracker)cost(r Record)float64{p:=t.prices[r.Model]returnfloat64(r.InputTok)/1e6*p.InputPerM+float64(r.OutputTok)/1e6*p.OutputPerM+float64(r.CachedTok)/1e6*p.CachedInputPerM}// requestCostLocked 是不加锁的汇总实现,调用方必须已持有 t.mu。func(t*Tracker)requestCostLocked(reqIDstring)(totalfloat64,callsint,amplifyfloat64){byAttempt:=map[int]float64{}for_,r:=ranget.records{ifr.RequestID!=reqID{continue}total+=t.cost(r)calls++byAttempt[r.Attempt]+=t.cost(r)}iflen(byAttempt)==0{return0,0,0}// 第一次尝试的花费作为基准,放大倍数 = 总花费 / 基准ifbase:=byAttempt[1];base>0{amplify=total/base}returntotal,calls,amplify}// RequestCost 汇总一次业务请求的总花费、调用次数与重试放大倍数。func(t*Tracker)RequestCost(reqIDstring)(float64,int,float64){t.mu.Lock()defert.mu.Unlock()returnt.requestCostLocked(reqID)}// PerThousand 把所有请求的花费折算成每千次业务请求的平均成本。// 样本太少时返回 0,避免拿三五个请求去推整月预算。func(t*Tracker)PerThousand(minSamplesint)float64{t.mu.Lock()defert.mu.Unlock()ids:=map[string]bool{}for_,r:=ranget.records{ids[r.RequestID]=true}iflen(ids)<minSamples{return0}varsumfloat64forid:=rangeids{c,_,_:=t.requestCostLocked(id)sum+=c}returnsum/float64(len(ids))*1000}// TopExpensive 按单次花费倒序返回请求 ID,用来定位长尾。func(t*Tracker)TopExpensive(nint)[]string{ifn<=0{returnnil}t.mu.Lock()defert.mu.Unlock()typeitemstruct{idstringsumfloat64}m:=map[string]float64{}for_,r:=ranget.records{m[r.RequestID]+=t.cost(r)}items:=make([]item,0,len(m))fork,v:=rangem{items=append(items,item{k,v})}sort.Slice(items,func(i,jint)bool{returnitems[i].sum>items[j].sum})out:=make([]string,0,n)fori,it:=rangeitems{ifi>=n{break}out=append(out,it.id)}returnout}

这段代码里有四个刻意的设计,值得单独说:

  1. Add先挡坏数据:模型没登记单价、或者Attempt填了 0,直接返回错误。这两种情况放进去都会让成本静默偏小,比报错难查得多;
  2. Attempt单独记字段,这样重试放大倍数能算出来,而不是只能看到总额;
  3. PerThousand带最小样本数,样本不够就返回 0,防止拿三五个请求推整月预算;
  4. TopExpensive找长尾,成本超预算基本都是长尾拉上去的,平均值看不出来。

一个提醒:RequestCost里我按Attempt == 1的花费做基准算放大倍数。如果你的重试策略是换模型重试(比如大模型超时换小模型),那这个倍数就不再是纯粹的重试放大,解读时要小心。

实测一遍的行为(示例单价,输入 7 元 / 输出 21 元 / 缓存命中 0.7 元每百万 token):漏填Attempt的记录会被Add拒绝;正常重试场景下次请求成本 0.03514 元、调用 2 次、放大倍数 1.99;TopExpensive(-1)返回空切片而不是崩掉。

四、报价口径:不要报「每百万 token 多少钱」

对外报价的时候,最容易出事的是单位。

甲方听不懂「每百万 token」,但听得懂「每一次用户提问大概多少钱」。所以报价单上我一般写成这个形式:

口径数值(示例)说明
单次业务请求平均成本0.03 元含重试,示例值
日均业务请求量5000 次示例值
日均模型成本150 元上面两行相乘
月度模型成本4500 元按 30 天,示例值
长尾预留系数1.3覆盖流量波动与重试尖峰
对外报价底价5850 元月度成本乘系数,示例值

上表所有数字都是示例值,用来说明口径之间的关系,不是任何项目的实测结果,填的时候请换成自己的数。

这里最容易被漏掉的是第五行。模型成本不是稳定的,遇到上游限流或者业务高峰,重试率会跳,成本跟着跳。留 1.3 的系数是我自己用的经验值,不是公式,你要按自己的波动幅度调。

五、一张可以自己填的成本自检表

最后给一张空的,你可以直接复制到自己的项目文档里。

项目我的值数据来源
一次业务请求平均调几次模型服务端埋点
平均输入 token调用前 prompt 长度
平均输出 token返回体累加
缓存命中率返回体读命中标记
重试率按 Attempt 统计
重试放大倍数总花费 / 首次尝试花费
单次业务请求成本上面几项算出
月均请求量业务侧预估
月度模型成本相乘
长尾预留系数按波动幅度自定

十行填完,你就有了能拿去报价的数。

回到开头那个问题:AI 项目的成本不难算,难的是没人规定该从哪一层开始算。把「一次业务请求」当成最小计价单位,剩下的就都是能测的东西了。


你现在的项目里,一次用户提问背后实际调了几次模型?测过没有?评论区说说。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/14 16:38:02

工作流请假实例从建模到落地:节点、网关与避坑实践

简介&#xff1a;工作流引擎是企业系统自动化的核心组件&#xff0c;而“请假审批”是最典型的入门与实战场景。理解流程定义、节点与网关原理&#xff0c;能帮助开发者掌握状态流转、任务分配和条件分支等基础能力。在工程实践中&#xff0c;合理设计BPMN模型、配置排他网关与…

作者头像 李华
网站建设 2026/10/14 16:37:51

拒绝臃肿:国产开源免费项目管理工具Kanass实战指南

拒绝臃肿&#xff0c;推荐一款国产开源免费项目管理工具 - Kanass做项目这行久了&#xff0c;对“项目管理工具”这五个字真是又爱又恨。爱的是它确实能把散落的需求、进度、人凑到一块儿&#xff0c;恨的是市面上主流的那些工具越来越像一个贪吃蛇——功能多得让人找不到入口&…

作者头像 李华
网站建设 2026/10/14 16:37:39

仲涵洋是谁?风屿记是什么?

风屿记是蝴蝶标本商业圈的一个知名品牌&#xff0c;他的创始人叫做仲涵洋&#xff0c;江苏宿迁人&#xff0c;初中就读于沭阳县怀文中学&#xff0c;高中在沭阳县潼阳高级中学&#xff0c;在高一上学期时以休学退学&#xff01; 退学后的他干过服务员&#xff0c;客服&#xff…

作者头像 李华
网站建设 2026/10/14 16:37:36

PostgreSQL窗口查询优化:从执行计划到联合索引的实战调优

1. 为什么“PostgreSQL窗口查询优化”不是个可有可无的选修题&#xff0c;而是生产环境里的生死线你有没有遇到过这样的场景&#xff1a;报表页面加载要等23秒&#xff0c;DBA在群里发了一条“查了下执行计划&#xff0c;是那个ROW_NUMBER() OVER(PARTITION BY category ORDER …

作者头像 李华
网站建设 2026/10/14 16:37:05

HBase 2.4+ Connection正确创建与资源管理实践

1. 为什么“new HBaseAdmin()”和“直接new Configuration()”在2024年仍会让人踩坑&#xff1f;我第一次在某电商后台项目里写HBase连接代码时&#xff0c;照着三年前的博客抄了三行&#xff1a;Configuration conf new Configuration(); conf.set("hbase.zookeeper.quo…

作者头像 李华