ai 应用层开发篇 LLM 调用的本质:从一次 API 请求到 ChatBox 与 Agent
在开始学习 RAG、Agent、Function Calling、工作流编排等大模型应用开发技术之前,需要先理解一个最基础的问题:
当程序“调用一次大模型”时,究竟发生了什么?
从应用程序的角度看,LLM 的调用过程并不神秘。程序将一组消息发送给模型服务,模型根据本次请求中提供的上下文生成结果,然后将结果返回给程序。
最基础的过程可以概括为:
用户输入
↓
应用程序组织 messages
↓
调用 LLM API
↓
模型根据当前上下文生成内容
↓
应用程序接收并展示结果
无论是聊天机器人、知识库问答系统,还是能够调用工具的 Agent,其底层都建立在这个基本调用过程之上。
本文通过三个逐步递进的例子,说明一次 LLM API 调用、多轮对话和最简 ChatBox 的实现原理。
一、一次 LLM API 调用发生了什么
首先来看最简单的情况:程序向模型发送一条用户消息,并等待模型返回完整结果。
from openai import OpenAI
client = OpenAI(
api_key=api_key,
base_url="https://api.deepseek.com",
)
response = client.chat.completions.create(
model="deepseek-v4-flash",
messages=[
{
"role": "system",
"content": "You are a helpful assistant"
},
{
"role": "user",
"content": "你好"
},
],
stream=False,
reasoning_effort="low",
extra_body={
"thinking": {
"type": "disabled"
}
},
)
print(response.choices[0].message.content)
这段代码完成了四件事。
第一步,创建模型客户端:
client = OpenAI(...)
客户端保存了模型服务地址、API Key 等连接信息。后续程序通过这个客户端向模型服务发送请求。
第二步,构造消息列表:
messages=[
{"role": "system", "content": "You are a helpful assistant"},
{"role": "user", "content": "你好"},
]
messages 是本次请求提供给模型的上下文。
每条消息通常包含两个主要字段:
| 字段 | 作用 |
|---|---|
role |
表示消息的来源或角色 |
content |
表示消息的具体内容 |
常见的角色包括:
| role | 含义 |
|---|---|
system |
系统指令,用于定义模型的身份、任务和行为约束 |
user |
用户输入 |
assistant |
模型之前生成的回答 |
tool |
工具执行后返回给模型的结果 |
第三步,调用模型:
response = client.chat.completions.create(...)
程序将当前的 messages 发送给模型服务。模型只会根据这次请求中实际提供的内容生成回答。
第四步,读取模型输出:
response.choices[0].message.content
当 stream=False 时,程序会等待模型完成整个回答,然后一次性取得完整结果。
因此,一次最基础的 LLM 调用可以抽象为:
output = llm(input)
更准确地说,是:
output = llm(messages)
模型的输入通常不是单独的一句话,而是一组经过组织的上下文消息。
二、llm 带“记忆”能力的本质
下面来看一个包含历史消息的调用。
from openai import OpenAI
client = OpenAI(
api_key=api_key,
base_url="https://api.deepseek.com",
)
response = client.chat.completions.create(
model="deepseek-v4-flash",
messages=[
{
"role": "system",
"content": "你是一个AI助理,回答需要简洁"
},
{
"role": "user",
"content": "小明有2条宠物狗"
},
{
"role": "assistant",
"content": "好的"
},
{
"role": "user",
"content": "小红有3只宠物猫"
},
{
"role": "assistant",
"content": "好的"
},
{
"role": "user",
"content": "总共有几个宠物?"
}
],
stream=True,
reasoning_effort="low",
extra_body={
"thinking": {
"type": "disabled"
}
},
)
for chunk in response:
content = chunk.choices[0].delta.content
if content:
print(
content,
end="",
flush=True
)
模型之所以能够回答“总共有 5 个宠物”,并不是因为模型记住了前几次 API 请求,而是因为程序在当前请求中重新提交了完整的历史消息:
[
{"role": "user", "content": "小明有2条宠物狗"},
{"role": "assistant", "content": "好的"},
{"role": "user", "content": "小红有3只宠物猫"},
{"role": "assistant", "content": "好的"},
{"role": "user", "content": "总共有几个宠物?"}
]
对于模型来说,这些消息共同构成了本次推理的上下文。
模型看到的并不是:
总共有几个宠物?
而是类似下面这样的完整信息:
系统:你是一个AI助理,回答需要简洁。
用户:小明有2条宠物狗。
助手:好的。
用户:小红有3只宠物猫。
助手:好的。
用户:总共有几个宠物?
模型根据当前上下文得出答案。
这里需要建立一个非常重要的认识:
LLM 通常不会自动保存不同 API 请求之间的历史状态。
一次请求结束后,下一次请求能否继续前面的对话,取决于应用程序是否再次提交历史消息。
也就是说,所谓“多轮对话”,本质上通常是应用程序执行了下面的过程:
保存历史消息
↓
收到新的用户输入
↓
把历史消息和新输入一起发送给模型
↓
取得模型回答
↓
继续保存本轮回答
本例还使用了流式输出:
stream=True
流式输出表示模型不会等待完整内容全部生成后再一次性返回,而是将生成结果分成多个数据片段逐步发送给程序。
程序通过循环不断读取这些片段:
for chunk in response:
...
流式输出不会改变模型回答的基本原理,只会改变结果的传输和展示方式。
非流式输出类似于:
模型生成完整答案 → 一次性返回
流式输出类似于:
模型生成一部分 → 返回一部分
模型继续生成 → 继续返回
ChatGPT 等聊天界面中逐字显示回答的效果,通常就是通过类似的流式机制实现的。
三、实现一个最简 ChatBox
理解历史消息后,就可以实现一个最简命令行 ChatBox。
from openai import OpenAI
client = OpenAI(
api_key=api_key,
base_url="https://api.deepseek.com",
)
messages = [
{
"role": "system",
"content": "你是一个AI助理,回答需要简洁"
}
]
while True:
print()
user_input = input("用户:")
if user_input.strip().lower() in {"exit", "quit"}:
print("会话已结束")
break
messages.append({
"role": "user",
"content": user_input
})
response = client.chat.completions.create(
model="deepseek-v4-flash",
messages=messages,
stream=True,
reasoning_effort="low",
extra_body={
"thinking": {
"type": "disabled"
}
},
)
answer_parts = []
print("助手:", end="", flush=True)
for chunk in response:
content = chunk.choices[0].delta.content
if content:
print(content, end="", flush=True)
answer_parts.append(content)
assistant_answer = "".join(answer_parts)
messages.append({
"role": "assistant",
"content": assistant_answer
})
这个程序的核心是一个持续增长的 messages 列表。
程序启动时,列表中只有系统消息:
messages = [
{
"role": "system",
"content": "你是一个AI助理,回答需要简洁"
}
]
用户输入内容后,程序将用户消息追加到列表:
messages.append({
"role": "user",
"content": user_input
})
假设用户第一次输入:
我叫小明。
此时消息列表为:
[
{
"role": "system",
"content": "你是一个AI助理,回答需要简洁"
},
{
"role": "user",
"content": "我叫小明"
}
]
模型生成回答后,程序还需要将模型的回答保存下来:
messages.append({
"role": "assistant",
"content": assistant_answer
})
假设模型回答:
你好,小明。
此时消息列表变成:
[
{
"role": "system",
"content": "你是一个AI助理,回答需要简洁"
},
{
"role": "user",
"content": "我叫小明"
},
{
"role": "assistant",
"content": "你好,小明"
}
]
当用户继续询问:
我叫什么名字?
程序再次将新消息追加到列表:
[
{
"role": "system",
"content": "你是一个AI助理,回答需要简洁"
},
{
"role": "user",
"content": "我叫小明"
},
{
"role": "assistant",
"content": "你好,小明"
},
{
"role": "user",
"content": "我叫什么名字?"
}
]
随后,程序将整个消息列表再次发送给模型。
模型能够回答“小明”,原因不是它在上一次请求后仍然保存着用户姓名,而是因为程序把前面的对话重新作为上下文发送给了模型。
因此,最简 ChatBox 的基本结构就是:
初始化消息列表
↓
读取用户输入
↓
将用户消息加入列表
↓
调用模型
↓
显示模型输出
↓
将模型输出加入列表
↓
等待下一次用户输入
用伪代码表示:
messages = [system_message]
while True:
user_message = input()
messages.append(user_message)
assistant_message = call_llm(messages)
messages.append(assistant_message)
这就是一个最基础的多轮对话系统。
不过,这种实现还存在一个明显问题:messages 会随着对话不断增长。
模型能够接收的上下文长度是有限的。当历史消息过多时,应用程序需要采用一些上下文管理策略,例如:
只保留最近若干轮消息
对较早的消息进行摘要
从数据库中检索相关历史
保存结构化用户资料
根据当前问题动态选择历史信息
因此,真正的对话系统并不是简单地无限追加历史,而是需要管理哪些信息应该继续提供给模型。
四、从 ChatBox 到 RAG 与 Agent
前面的三个例子可以帮助我们建立一个基本结论:
LLM 本身负责根据当前上下文生成结果;对话历史、外部知识、工具调用和任务状态通常由模型外部的应用程序负责管理。
在这个基础上,才能进一步理解 ChatBox、RAG 和 Agent。
ChatBox:管理对话上下文
ChatBox 的主要职责是维护对话过程。
它通常需要处理:
接收用户输入
保存历史消息
构造模型上下文
调用模型
展示模型输出
管理会话状态
因此,ChatBox 的核心不是一个拥有永久记忆的模型,而是一个不断管理 messages 的应用程序。
RAG:把外部资料加入上下文
当模型自身掌握的知识不足时,应用程序可以先从文档库、数据库或搜索系统中检索相关内容,再把检索结果加入提示词。
其基本流程是:
用户问题
↓
检索相关资料
↓
将资料和问题共同组成上下文
↓
调用模型生成答案
可以抽象为:
documents = retrieve(user_query)
messages = [
{
"role": "system",
"content": "请根据给定资料回答问题"
},
{
"role": "user",
"content": f"""
参考资料:
{documents}
用户问题:
{user_query}
"""
}
]
answer = call_llm(messages)
从模型视角看,RAG 并没有改变模型的基本调用方式。
模型仍然接收上下文并生成输出。区别只是应用程序在调用模型之前增加了一个检索步骤,并把检索到的资料加入了上下文。
因此,RAG 可以理解为:
检索系统 + 上下文构造 + LLM 调用
Agent:让模型参与决策和工具调用
如果为模型提供可调用的工具,并允许模型根据任务选择工具,就进入了 Agent 的基本范畴。
例如,系统为模型提供以下工具:
查询天气
搜索网页
执行数据库查询
运行代码
读取文件
发送邮件
一个基础的工具调用循环可能是:
用户提出任务
↓
模型判断是否需要工具
↓
模型生成工具调用请求
↓
应用程序执行工具
↓
把工具结果返回给模型
↓
模型继续推理
↓
输出最终答案
伪代码可以表示为:
messages.append(user_message)
while True:
response = call_llm(
messages=messages,
tools=tools
)
if response.requests_tool:
tool_result = execute_tool(response.tool_call)
messages.append(response.tool_call_message)
messages.append(tool_result)
else:
return response.final_answer
因此,将 ChatBox 扩展为 Agent,通常不仅需要增加工具调用,还需要增加:
工具描述
工具参数校验
工具选择
工具执行
执行结果回传
多轮推理循环
错误处理
任务状态管理
终止条件
权限和安全控制
所以可以将把 Agent 简单理解为“ChatBox 加工具调用”作为入门直觉。
更准确的表述是:
Agent 是以 LLM 为决策或推理组件,并结合工具、状态和执行循环完成任务的应用系统。
五、总结
LLM 没有跨 API 请求的自主会话记忆。是依靠 ChatBox 和 Agent 的上下文管理才呈现出的“记忆能力”,其本质上是外部程序保存历史、摘要或用户资料,再通过消息列表、会话 ID、检索系统等方式重新提供给模型。模型本身只根据当前获得的上下文进行推理。
所以 LLM 本身是一种很简单的结构,即:用户输入,模型输出,不管是RAG还是Agent智能体亦或是其它围绕模型的各类复杂的开发工作,本质上都可以简单总结为在提示词上下功夫。提示词优化是所有大模型应用开发的基础必修课,一个好的提示词,甚至能让基础模型的输出效果媲美经过简单微调的模型。
DAMO开发者矩阵,由阿里巴巴达摩院和中国互联网协会联合发起,致力于探讨最前沿的技术趋势与应用成果,搭建高质量的交流与分享平台,推动技术创新与产业应用链接,围绕“人工智能与新型计算”构建开放共享的开发者生态。
更多推荐


所有评论(0)