本系列共 10 阶段 ,面向有 Go 后端基础的开发者,全程用 Go 手写实现。
当前进度:阶段一 大模型基础 · 第 3 课
上一课我们学习了大语言模型如何根据上下文逐步生成 Token。这一课进一步回答三个实际开发问题:
- 为什么中文字符数和模型处理的 Token 数量不一样?
- 为什么多轮对话不能无限地保留历史消息?
- 为什么同样调用一次模型,费用和响应时间可能差别很大?
这些问题不只是模型知识,也会直接影响后续 Go AI 应用的接口设计、会话存储和成本控制。
一、本课学习目标
学完本课,你应该能够:
- 理解 Token、上下文窗口和最大输出 Token 数的区别。
- 了解模型调用时哪些内容会占用上下文。
- 理解多轮对话为什么需要管理历史消息。
- 掌握模型调用成本的基本计算方式。
- 能够设计一个简单的 Go 对话上下文管理方案。
二、Token 到底是什么?
Token 是模型处理文本时使用的基本单位。它不严格等于汉字、英文单词或 Go 字符串中的一个字符。
例如,下面这句话:
text
我正在学习 Go AI Agent。
不同模型使用的 Tokenizer 可能产生不同的切分结果。我们可以把它示意为:
text
["我", "正在", "学习", " Go", " AI", " Agent", "。"]
这只是示例,不是实际模型的 Token 切分结果。
英文也一样:
text
Go is a programming language.
可能被切分成若干 Token,其中一个 Token 可能是完整单词,也可能是单词的一部分。
为什么不能直接用字符串长度计算 Token?
假设你在 Go 中写:
go
question := "我正在学习 Go AI Agent"
fmt.Println(len(question))
这里的 len(question) 返回的是 UTF-8 编码后的字节数,不是 Token 数,也不是 Unicode 字符数量。
如果需要计算 Unicode 码点数量,可以使用:
go
fmt.Println(utf8.RuneCountInString(question))
但即使得到码点数量,也不能直接认为这就是 Token 数量。
因此,在实际项目中:
- 字符串长度适合用于基础输入长度限制。
- Rune 数量适合用于某些字符级统计。
- Token 数量应该通过对应模型的 Tokenizer 估算,或者使用服务商提供的用量信息。
不同模型的 Tokenizer 可能不同,所以不要使用一个固定比例来精确计算所有中英文文本的 Token 数量。
三、什么是上下文窗口?
上下文窗口(Context Window)指模型在一次推理中能够处理的上下文容量,通常以 Token 为单位描述。
可以把它理解为模型在当前这次请求中能够利用的信息预算。
假设某个模型支持的上下文容量为 32,000 Token。为了便于计算,我们暂时把它当作一个示例模型。
一次请求中可能包含:
- System 消息:系统级指令。
- 历史对话:此前的 User 和 Assistant 消息。
- 当前问题:用户这次提出的问题。
- 工具信息:工具定义、工具调用结果等。
- 模型输出:这次生成的回答。
这些内容都会影响上下文容量的使用情况。具体的计数方式和额外开销取决于模型与 API 的实现。
示例:32,000 Token 的上下文预算
以下是用于教学的假设分配,不代表某个具体模型的真实配置。
| 组成 | 占用 Token |
|---|---|
| 系统指令与工具定义 | 2,000 |
| 历史对话 | 12,000 |
| 当前问题及相关资料 | 8,000 |
| 预留给模型输出 | 4,000 |
| 剩余余量 | 6,000 |
在这个例子中,已经使用了 22,000 Token,预留了 4,000 Token 输出,另外留有 6,000 Token 余量。
需要注意:这不是要求每次请求都必须填满窗口。很多时候,简洁的上下文反而更经济、更容易维护。
四、为什么多轮对话会越来越占用 Token?
先看一个常见的聊天场景。
用户第一轮问:
什么是 Go 的 Context?
第二轮问:
它和 context.WithTimeout 有什么关系?
第三轮问:
能不能给我一个实际的 HTTP 请求示例?
第三个问题中的"它"需要结合前面的聊天记录理解。
如果应用只把第三个问题发送给模型,模型可能不知道"它"具体指什么。因此,应用通常需要把相关历史消息一并发送。
例如:
json
[
{
"role": "user",
"content": "什么是 Go 的 Context?"
},
{
"role": "assistant",
"content": "Context 用于传递取消信号、截止时间等信息。"
},
{
"role": "user",
"content": "它和 context.WithTimeout 有什么关系?"
},
{
"role": "assistant",
"content": "WithTimeout 可以创建带超时时间的 Context。"
},
{
"role": "user",
"content": "能不能给我一个实际的 HTTP 请求示例?"
}
]
注意:这里的 role 表示消息角色,content 表示消息内容。后面我们会专门学习这些消息结构。
每次调用模型时,如果应用重复发送历史消息,那么早期的对话也会反复占用输入 Token。
这意味着:
- 对话越长,输入 Token 通常越多。
- 输入 Token 越多,调用成本可能越高。
- 长上下文可能增加请求处理时间。
- 上下文超过限制时,请求可能失败,或者需要先裁剪内容。
一个容易忽略的细节
如果你连续调用模型三次,并且每次都发送全部历史记录,那么第二次和第三次请求中的部分内容,其实已经在前面的请求中出现过。
但对于普通 API 请求来说,不能默认服务商会自动帮你保存对话,也不能默认重复内容完全免费。某些服务可能提供 Prompt 缓存等机制,具体计费要看服务商规则。
五、上下文管理应该怎么设计?
这是 Go 后端工程师需要掌握的内容。
假设你的 AI 对话系统使用 MySQL 保存消息,数据表可以简化为:
sql
CREATE TABLE chat_messages (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
conversation_id BIGINT NOT NULL,
role VARCHAR(20) NOT NULL,
content TEXT NOT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
这里的 conversation_id 用于区分不同会话。
当用户发来新消息时,后端可以采用以下流程:
text
接收用户消息
↓
保存当前消息
↓
读取当前会话的历史记录
↓
根据 Token 预算选择需要的历史消息
↓
组装模型请求
↓
调用 LLM API
↓
保存模型回答
↓
返回结果
注意,不能简单地把所有历史记录都读取出来并发送给模型。随着对话增长,需要根据预算选择保留哪些内容。
常见策略有三种:
| 策略 | 工作方式 | 适用场景 |
|---|---|---|
| 最近消息窗口 | 只保留最近若干轮对话 | 普通聊天 |
| 历史摘要 | 将较早的对话压缩成摘要 | 长时间持续的会话 |
| 检索相关历史 | 根据当前问题查找相关的旧消息 | 历史记录较多、主题跨度较大的场景 |
实际系统可以组合使用这些策略。
例如,保留最近 10 轮对话,同时维护一份历史摘要;如果用户提到以前讨论过的项目,再按需检索相关历史。
需要注意:摘要和检索都可能遗漏重要信息,因此涉及权限、订单状态等关键业务数据时,应该重新查询权威数据源,而不是依赖聊天历史。
六、模型调用成本怎么算?
假设某个模型服务的计价规则如下。这里的数字只是为了教学而设定的示例价格,不代表任何实际服务商的报价。
- 输入:每百万 Token 0.50 美元。
- 输出:每百万 Token 2.00 美元。
一次请求消耗:
- 输入 Token:10,000。
- 输出 Token:2,000。
计算公式为:
text
总费用 = 输入 Token ÷ 1,000,000 × 输入单价
+ 输出 Token ÷ 1,000,000 × 输出单价
代入示例:
text
10,000 ÷ 1,000,000 × 0.50 = 0.005
2,000 ÷ 1,000,000 × 2.00 = 0.004
合计 = 0.009
因此,这次请求的示例费用是 0.009 美元。
如果每天有 10,000 次类似请求,那么仅按照这个假设的价格计算:
text
10,000 × 0.009 = 90 美元/天
真实费用还会受到模型选择、输入输出比例、缓存折扣、工具调用次数以及服务商计价规则等因素影响。
为什么 Agent 的成本更值得关注?
普通聊天可能只调用一次模型。
但一个 Agent 可能执行:
text
第 1 次调用:分析任务
↓
第 2 次调用:决定调用搜索工具
↓
执行工具
↓
第 3 次调用:分析搜索结果
↓
第 4 次调用:生成最终回答
如果每次调用都重复携带较长的上下文,累计 Token 消耗就可能明显增加。
因此,后续开发 Agent 时,我们会引入最大迭代次数、Token 预算、超时控制等机制,避免任务无限执行或成本失控。
七、动手理解:Token 预算怎么算
下面用一个具体数字走一遍,直观感受输入和输出如何共同占用上下文。
| 项目 | Token |
|---|---|
| 上下文窗口 | 32,000 |
| 输入 Token | 18,000 |
| 预留输出 Token | 4,000 |
| 额外预留余量 | 2,000 |
| 合计占用 | 24,000 |
| 剩余 | 8,000 |
在当前简化预算内,剩余 8,000 Token。
这是简化的教学计算。实际模型还可能有额外 Token 开销、不同的输入输出限制,以及独立的最大输出长度约束。
八、本课总结
本课最重要的是掌握以下概念:
- Token 是模型处理文本的基本单位,不能直接等同于字符串长度。
- 上下文窗口限制一次推理可以处理的信息容量。
- 多轮对话需要管理历史消息,不能无条件地无限累积。
- 模型调用成本通常与输入、输出 Token 数量以及具体计价规则有关。
- Go 后端应该通过历史裁剪、摘要、检索、超时和预算控制,让 AI 应用保持可控。
九、课后练习
练习 1:理解上下文窗口
一个模型的上下文窗口为 16,000 Token。某次请求的输入占用 12,000 Token,预留输出为 3,000 Token,另外需要预留 1,500 Token 作为额外预算。按照本课的简化计算方式,预算是否足够?超出或剩余多少?
参考思路 :12,000 + 3,000 + 1,500 = 16,500,超过 16,000,超出 500 Token,预算不够。实际做法是把输入裁剪掉至少 500 Token,或者降低预留输出,否则请求可能被服务端直接拒绝。
练习 2:设计多轮对话
一个用户与 AI 已经交流了 200 轮。你会如何设计历史消息管理,既保留必要上下文,又避免每次请求都发送全部历史?请说明你的方案。
参考思路:三层组合。① 滑窗:始终带上最近 N 轮原文;② 摘要:更早的对话异步压缩成一段摘要,放在 System 之后;③ 检索:会话里的关键事实(项目名、版本、报错信息)单独存一张结构化表,按需注入。消息原表放 MySQL,热会话和摘要放 Redis。注意权限、余额、订单状态这类数据永远重新查权威库,不依赖聊天历史。
练习 3:成本计算
假设输入价格为每百万 Token 1 美元,输出价格为每百万 Token 4 美元。一次请求输入 5,000 Token、输出 1,000 Token,调用 1,000 次总费用是多少?(忽略缓存及其他费用)
参考思路 :单次 = 5,000 ÷ 1,000,000 × 1 + 1,000 ÷ 1,000,000 × 4 = 0.005 + 0.004 = 0.009 美元;1,000 次 = 9 美元。注意输出单价是输入的 4 倍,所以"让模型少说废话"往往比"少给点上下文"更省钱。
下一篇:Go AI Agent 实战 04|Prompt 与消息角色:System、User、Assistant