在开始学习 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智能体亦或是其它围绕模型的各类复杂的开发工作,本质上都可以简单总结为在提示词上下功夫。提示词优化是所有大模型应用开发的基础必修课,一个好的提示词,甚至能让基础模型的输出效果媲美经过简单微调的模型。

Logo

DAMO开发者矩阵,由阿里巴巴达摩院和中国互联网协会联合发起,致力于探讨最前沿的技术趋势与应用成果,搭建高质量的交流与分享平台,推动技术创新与产业应用链接,围绕“人工智能与新型计算”构建开放共享的开发者生态。

更多推荐