上下文窗口与 Function Calling:AI 的记忆与双手
上下文窗口与 Function Calling:AI 的记忆与双手
如果说提示工程解决的是"如何对AI说话",那么上下文窗口和Function Calling解决的就是"AI能记住多少"以及"AI能替我们做什么"。上下文窗口是大语言模型工作记忆的物理边界,Function Calling则是大语言模型伸向真实世界的双手。这两个机制看似基础,却是所有实用AI应用(从长对话助手到能操作数据库的Agent)的地基。
🧠 上下文窗口:AI 的工作记忆
什么是上下文窗口
大语言模型并不是像人一样拥有持久记忆的大脑,它更像一块每次对话都要重新擦写的白板。上下文窗口(Context Window)指的是模型单次推理时能够"看到"的token总量上限,这个上限同时涵盖了系统提示、历史对话、用户当前输入,以及模型即将生成的回复。
这里的token并不等于汉字或单词,而是模型分词器(Tokenizer)切分文本后的最小单元。一段中文文本平均每个汉字大约消耗1.5~2个token,英文单词则大约每4个字符消耗1个token。当对话中的文本总量(历史消息+新输入+待生成内容)超过模型的上下文窗口上限时,模型要么直接报错拒绝处理,要么由应用层自动截断掉最早的部分内容。
可以把它想象成开会时的白板:白板面积是固定的,写满了就必须擦掉最早写的内容才能继续记录新内容——被擦掉的部分,参会者(模型)就再也看不到了。
为什么长对话会"失忆"
很多人第一次和AI助手长时间对话后会发现,模型突然"忘记"了几十轮之前提到的关键设定,比如自己反复强调过的姓名、偏好或任务背景。这并不是模型"变笨"了,而是两个因素共同作用的结果:
理解这一点很重要:上下文窗口大不代表模型会均匀地"记住"里面的每一句话,窗口只是决定了"能不能看到",而不是"看得有多认真"。
上下文窗口的进化史
从早期模型到今天,上下文窗口经历了近三个数量级的扩张:
| 阶段 | 代表模型 | 上下文窗口量级 |
| --- | --- | --- |
| 早期 | GPT-3 | 2K~4K tokens |
| 成长期 | GPT-3.5 / GPT-4 初版 | 4K~32K tokens |
| 扩展期 | Claude 2 / GPT-4 Turbo | 100K~128K tokens |
| 长文本期 | Claude 3 系列 | 200K tokens |
| 超长文本期 | Gemini 1.5/2.0 Pro | 1M~2M tokens |
短短几年时间,主流模型的上下文窗口从几千token跃升到百万token级别,这意味着理论上可以一次性把一整本书、一个中型代码仓库或者数百页文档全部塞进一次请求里,不再依赖分段处理。
不过需要提醒一点:厂商宣传的窗口上限是"理论容量",并不完全等于"有效可用容量"。很多评测发现,当输入接近窗口上限时,模型的回答质量、指令遵循能力会出现不同程度的下降,这也是前面提到的"Lost in the Middle"现象的另一种体现。因此在实际工程中,即便模型支持100万token窗口,也未必意味着应该把它用满,还是要结合实测效果来决定合理的输入长度。
更大的窗口,更贵的代价
上下文窗口并非越大越好,它直接牵动两个现实约束:
因此,工程实践中往往不是"能用多大窗口就用多大",而是"够用就好"——盲目把全部历史都塞进上下文,既费钱又拖慢体验,收益却未必成正比。
如何应对有限的上下文
面对窗口有限、成本敏感的现实,业界常见的应对策略包括:
这几种策略并不互斥,实际系统里通常是"滑动窗口保留近期对话 + 摘要浓缩中期信息 + 外部检索补充长期记忆"三者组合使用。
一个直观的成本对照
假设某模型输入token单价是每百万token10元,输出单价是每百万token30元:
同样一次提问,仅仅因为携带了更长的历史上下文,成本就可能相差40倍以上。而且要注意,多轮对话中每一轮通常都要把之前所有历史重新发送一遍(模型本身没有"记忆",每次都是从零读取上下文),这意味着对话越长,每一轮的成本和延迟都会随之走高,而不是只有第一轮贵。这也是为什么"控制上下文长度"本身就是一项重要的工程优化,而不只是学术话题。
🛠️ Function Calling:AI 长出双手
从"只会说话"到"能做事"
原生的大语言模型本质上是一个文本生成器:给它一段文字,它续写出下一段文字。它不会主动去查今天的天气、不会真的帮你订机票、也无法读取你数据库里的实时数据——因为它的所有"知识"都封存在训练数据里,训练截止之后发生的一切它都不知道,更没有手脚去操作外部系统。
Function Calling(函数调用/工具调用)正是为了解决这个问题而生的机制。它让模型不再局限于"输出自然语言",而是可以在必要时输出一段结构化的"我想调用哪个工具、传什么参数"的指令,再由外部程序真正执行这个动作,把结果反馈回来供模型继续对话。这一步,是AI从"纸上谈兵"迈向"动手实践"的关键转折点。
工作原理:三步握手
Function Calling的运作可以拆解为一个清晰的三段式流程:
整个过程中,模型从始至终不会真的"跑代码"或"发请求",它只负责"判断该调用谁、该传什么参数"这一层决策,真正的执行权始终掌握在宿主程序手里——这也是Function Calling相对安全可控的原因之一。
完整示例:一次天气查询的往返
假设我们要做一个能查天气的助手,先给模型声明一个工具(以OpenAI风格的tools参数为例):
{
"model": "gpt-4o",
"messages": [
{ "role": "user", "content": "上海今天天气怎么样?需要带伞吗?" }
],
"tools": [
{
"type": "function",
"function": {
"name": "get_weather",
"description": "查询指定城市当前的实时天气情况",
"parameters": {
"type": "object",
"properties": {
"city": {
"type": "string",
"description": "城市名称,例如:上海"
},
"unit": {
"type": "string",
"enum": ["celsius", "fahrenheit"],
"description": "返回的温度单位"
}
},
"required": ["city"]
}
}
}
]
}模型读到用户问题后,判断"实时天气"超出了自己的知识范围,必须调用工具,于是返回的不是一段闲聊文字,而是一个结构化的调用请求:
{
"choices": [
{
"message": {
"role": "assistant",
"content": null,
"tool_calls": [
{
"id": "call_9f2k7a",
"type": "function",
"function": {
"name": "get_weather",
"arguments": {
"city": "上海",
"unit": "celsius"
}
}
}
]
},
"finish_reason": "tool_calls"
}
]
}⚠️ 需要说明:真实的OpenAI API中,arguments字段实际上是一段JSON格式的字符串,需要开发者自己用JSON.parse()反序列化成对象后才能使用;这里为了方便阅读直接展示成对象形式。
宿主程序收到这个调用请求后,真正去调用天气服务查到"上海、小雨、26℃",然后把结果作为一条新消息追加回对话历史,再次发给模型:
{
"role": "tool",
"tool_call_id": "call_9f2k7a",
"content": { "city": "上海", "temperature": 26, "condition": "小雨" }
}模型基于这个真实结果,最终生成给用户的自然语言回复:"上海今天有小雨,气温26℃左右,建议出门带一把伞。"从用户提问到最终回答,模型全程没有编造任何天气数据,所有事实都来自宿主程序真实执行工具后返回的结果,这正是Function Calling相比让模型"凭记忆瞎猜"更可靠的核心原因。
一个工具,无数可能
get_weather只是最简单的示范,实践中工具可以是查数据库、发送邮件、调用搜索引擎、执行代码、操作文件系统、下单支付等任意能被封装成函数的能力。只要清楚地描述好参数结构,模型就能学会在合适的时机选择合适的工具、拼出合理的参数——这一步,是让AI从"只会说话"进化成"能操作外部世界"的关键一步,也是构建能自主完成多步任务的AI Agent的基础能力之一。
一次对话中模型也不局限于只调用一个工具:面对复杂请求时,模型可以在一次响应中同时提出多个并行的工具调用(比如同时查询三座城市的天气),也可以先调用一个工具拿到结果后,再根据结果决定是否需要调用第二个工具,形成"调用—返回—再调用"的多轮链条,直到收集到足够信息才生成最终答案。
落地时容易踩的坑
Function Calling用起来很自然,但工程落地时有几个坑需要格外小心:
上下文窗口决定了AI一次能装下多少信息,Function Calling决定了AI能对世界做出多少改变——理解并用好这两个机制,才能真正把大语言模型从一个聊天玩具,变成一个能落地干活的生产力工具。