文章

LLM 与 Agent 开发入门

1. 大语言模型基础

大型语言模型(Large Language Models,LLM)是一种基于深度学习的神经网络系统,通过在海量文本数据上进行训练,习得了人类语言的模式与规律。这使得它们能够理解上下文、回答问题、生成创意内容、编写代码等多种复杂任务。本指南将系统地介绍LLM的核心概念、工作原理、能力与限制。

1.1 LLM 的基本原理

LLM的核心工作原理是通过分析输入的文本序列,预测下一个最可能出现的词或标记(token)。这种看似简单的任务,经过大规模训练后,使模型获得了惊人的语言理解和生成能力。

1.1.1 从机器学习流程到 LLM

和传统的机器学习类似,LLM 的原理本质也为:准备数据 → 定义模型 → 选择损失函数 → 应用优化算法 → 训练循环 → 评估性能。

  • 数据准备:

    • 收集大规模(TB级别)文本语料库(如维基百科、书籍、网页文本等)进行清洗和处理

    • 通过子词切分(如BPE算法)将文本转化为语义单元(Token)的向量表示,结合位置编码保留序列信息,形成高维稠密特征矩阵。

  • 模型定义:

    • 定义 Transformer 架构,设计超参数、上下文窗口大小、特殊标记、位置编码等;

    • 定义了参数的大小。

  • 损失函数:

    • 使用交叉熵损失进行标记预测、文本分类等。

  • 训练循环:进行多阶段训练,将于 1.2.1 详细介绍

    • 预训练:在大规模无标注文本上进行自监督学习

    • 微调:在特定任务的数据上进行有监督学习

    • RLHF 对齐:强化学习

  • 评估性能。

1.1.2 Transformer 架构介绍

在处理序列数据(如文本)时,早期NLP模型主要使用循环神经网络(RNN)、长短期记忆网络(LSTM)或门控循环单元(GRU)。这些模型虽然能捕捉序列中的时序关系,但存在两个主要问题:

  1. 难以并行计算(需要顺序处理)

  2. 长序列信息传递中的梯度消失问题

2017年,Google在论文《Attention Is All You Need》中提出了Transformer架构,通过自注意力(Self-Attention)机制解决了这些问题。

Self-Attention 机制可以看作是一种"动态卷积",它能根据上下文动态调整对不同位置的关注度:

  1. 对于输入序列中的每个元素(如单词),计算三个向量:查询(Query)、键(Key)和值(Value)

  2. 通过计算Query与所有Key的相似度,得到注意力分数

  3. 将注意力分数归一化(使用Softmax)

  4. 用这些分数对Value进行加权求和

这使得模型能够捕捉序列中任意距离的依赖关系,不受位置限制。

完整的 Transformer 架构包含编码器(Encoder)和解码器(Decoder)两部分:

  1. 编码器:处理输入序列,由多个相同的层堆叠而成,每层包含:

    • 多头自注意力(Multi-Head Self-Attention)机制

    • 前馈神经网络

    • 残差连接和层归一化

  2. 解码器:生成输出序列,结构类似编码器,但增加了:

    • 掩码多头自注意力(防止看到未来信息)

    • 编码器-解码器注意力(关注输入序列)

基于Transformer的两大模型家族:

  1. BERT:

    • 主要使用Transformer的编码器部分

    • 双向上下文理解(可以同时看到左右上下文)

    • 预训练任务:掩码语言模型(MLM)和下一句预测(NSP)

    • 适合理解任务(如分类、问答等)

  2. GPT系列:

    • 主要使用Transformer的解码器部分

    • 单向上下文理解(只能看到左侧上下文)

    • 预训练任务:下一个词预测

    • 适合生成任务(如文本生成、翻译等)

大语言模型(LLM)通常指的是GPT这类生成式模型,它们通过大规模的参数和训练数据,展现出强大的文本生成和理解能力。

1.2 LLM 核心技术概念

1.2.1 语言模型

如上述所说,大语言模型(LLM)是通过预测下一个词的监督学习方式进行训练的。

模型会根据当前输入 Context 预测下一个词(token) 的概率分布。通过不断比较模型预测和实际的下一个 token,并更新模型参数最小化两者差异,语言模型逐步掌握了语言的规律,学会了预测下一个 token。

在训练过程中,研究人员会准备大量句子或句子片段作为训练样本,要求模型一次次预测下一个 token,通过反复训练促使模型参数收敛,使其预测能力不断提高。经过在海量文本数据集上的训练,语言模型可以达到十分准确地预测下一个 token 的效果。这种以预测下一个token为训练目标的方法使得语言模型获得强大的语言生成能力

大型语言模型主要可以分为两类:基础语言模型和指令调优语言模型。

基础语言模型(Base LLM)通过反复预测下一个 token 来训练的方式进行训练,没有明确的目标导向。因此,如果给它一个开放式的 prompt ,它可能会通过自由联想生成戏剧化的内容。而对于具体的问题,基础语言模型也可能给出与问题无关的回答。例如,给它一个 Prompt ,比如”中国的首都是哪里?“,很可能它数据中有一段互联网上关于中国的测验问题列表。这时,它可能会用“中国最大的城市是什么?中国的人口是多少?”等等来回答这个问题。但实际上,您只是想知道中国的首都是什么,而不是列举所有这些问题。

相比之下,指令微调的语言模型(Instruction Tuned LLM)则进行了专门的训练,以便更好地理解问题并给出符合指令的回答。例如,对“中国的首都是哪里?”这个问题,经过微调的语言模型很可能直接回答“中国的首都是北京”,而不是生硬地列出一系列相关问题。指令微调使语言模型更加适合任务导向的对话应用。它可以生成遵循指令的语义准确的回复,而非自由联想。因此,许多实际应用已经采用指令调优语言模型。熟练掌握指令微调的工作机制,是开发者实现语言模型应用的重要一步。

那么,如何将基础语言模型转变为指令微调语言模型呢?这也就是训练一个指令微调语言模型(例如ChatGPT)的过程。

  • 首先,在大规模文本数据集上进行无监督预训练,获得基础语言模型。 这一步需要使用数千亿 token 甚至更多的数据,在大型超级计算系统上可能需要数月时间。

  • 之后,使用包含指令及对应回复示例的小数据集对基础模型进行有监督 fine-tune,这让模型逐步学会遵循指令生成输出,可以通过雇佣承包商构造适合的训练示例。 接下来,为了提高语言模型输出的质量,常见的方法是让人类对许多不同输出进行评级,例如是否有用、是否真实、是否无害等。

  • 然后,您可以进一步调整语言模型,增加生成高评级输出的概率。这通常使用基于人类反馈的强化学习(RLHF)技术来实现。

相较于训练基础语言模型可能需要数月的时间,从基础语言模型到指令微调语言模型的转变过程可能只需要数天时间,使用较小规模的数据集和计算资源即可。

1.2.2 参数

我们之前在学习机器学习的线性拟合的时候,接触了这个公式:

这里 w 和 b 其实就是参数,我们会用 BP 算法对参数进行拟合和优化。

在更复杂的 Transformer 等架构中,会出现相当数量的参数来进行初始化、梯度下降等。

现在的大模型几乎都是 Billion 级别的参数了。如:

  • GPT-3:175B(1750亿)

  • Qwen-72B:720亿参数

  • DeepSeek:提供了 1.5B~671B 不等参数的模型。

1.2.3 Token 与上下文窗口

token:

  • 定义:LLM处理文本的基本单位,可能是单词、词的一部分或标点符号

  • 重要性:API调用计费基于token数量,模型的上下文窗口也以token计算

  • 转换过程:文本通过分词器(tokenizer)转换为token序列

  • 语言差异:英文约0.75个token/单词,中文约1.5个token/字符

那么 token 是如何拆分的呢?

  • 例如,对于 "Learning new things is fun!" 这句话,每个单词都被转换为一个 token 。

  • 而对于较少使用的单词,可能会拆分为多个 token 。如 "Prompting as powerful developer tool",单词 "prompting" 会被拆分为三个 token,即"prom"、"pt"和"ing"。

得出结论:分词方式也会对语言模型的理解能力有影响。语言模型以 token 而非原词为单位进行建模,这一关键细节对分词器的选择及处理会产生重大影响。

当然,模型一次处理 token 的数量也是有限的。

上下文窗口:

  • 定义:模型一次能处理的最大token数量

  • 影响:决定了模型能"记住"多少之前的对话或文档内容

由于上下文窗口的限制,复杂任务通常依赖于微调,或用检索增强生成RAG等技术动态地向模型提供相关的上下文片段。

不同模型有不同的 token 限制,需要注意的是,这里的 token 限制是输入的 Prompt 和输出的 completion 的 token 数之和,因此输入的 Prompt 越长,能输出的 completion 的上限就越低。

ChatGPT3.5-turbo(2023) 的 token 上限是 4K,DeepSeek-V3 (2025)的 token 上限是 128K,Claude4 的 token 上限为 200K

1.3 实践:本地启动简单的模型

1.3.1 Pytorch 代码

以下是使用 PyTorch 实现的一个非常简单的基础语言模型

该代码由 Claude 生成。

import torch
import torch.nn as nn
import torch.nn.functional as F
from torch.utils.data import Dataset, DataLoader
from collections import Counter

# 1. 文本处理工具
class TextProcessor:
    def __init__(self, vocab_size=10000):
        self.vocab_size = vocab_size
        self.word2idx = {"<PAD>": 0, "<UNK>": 1, "<BOS>": 2, "<EOS>": 3}
        self.idx2word = {0: "<PAD>", 1: "<UNK>", 2: "<BOS>", 3: "<EOS>"}
        self.vocab_size = vocab_size

    def build_vocab(self, texts):
        words = []
        for text in texts:
            words.extend(text.split())

        counter = Counter(words)
        common_words = [word for word, _ in counter.most_common(self.vocab_size - 4)]

        for i, word in enumerate(common_words, 4):
            self.word2idx[word] = i
            self.idx2word[i] = word

    def encode(self, text):
        return [self.word2idx.get(word, self.word2idx["<UNK>"])
                for word in text.split()]

    def decode(self, indices):
        return " ".join([self.idx2word[idx] for idx in indices])


# 2. 数据集类
class SimpleDataset(Dataset):
    def __init__(self, texts, processor, seq_length=50):
        self.processor = processor
        self.seq_length = seq_length
        self.encoded_texts = []

        for text in texts:
            encoded = self.processor.encode(text)
            if len(encoded) > seq_length:
                encoded = encoded[:seq_length]
            else:
                encoded += [0] * (seq_length - len(encoded))
            self.encoded_texts.append(encoded)

    def __len__(self):
        return len(self.encoded_texts)

    def __getitem__(self, idx):
        encoded_text = self.encoded_texts[idx]
        x = torch.tensor(encoded_text[:-1])
        y = torch.tensor(encoded_text[1:])
        return x, y


# 3. 模型类
class SimpleLLM(nn.Module):
    def __init__(self, vocab_size, embedding_dim, hidden_dim, num_layers):
        super(SimpleLLM, self).__init__()
        self.embedding = nn.Embedding(vocab_size, embedding_dim)
        self.self_attention = nn.MultiheadAttention(
            embed_dim=embedding_dim,
            num_heads=8,
            batch_first=True
        )
        self.feed_forward = nn.Sequential(
            nn.Linear(embedding_dim, hidden_dim),
            nn.ReLU(),
            nn.Linear(hidden_dim, embedding_dim)
        )
        self.layer_norm1 = nn.LayerNorm(embedding_dim)
        self.layer_norm2 = nn.LayerNorm(embedding_dim)
        self.output_layer = nn.Linear(embedding_dim, vocab_size)

    def forward(self, x):
        embedded = self.embedding(x)
        attention_output, _ = self.self_attention(embedded, embedded, embedded)
        attention_output = self.layer_norm1(attention_output + embedded)
        ff_output = self.feed_forward(attention_output)
        ff_output = self.layer_norm2(ff_output + attention_output)
        output = self.output_layer(ff_output)
        return F.log_softmax(output, dim=-1)


# 4. 训练函数
def train_model(model, train_loader, num_epochs, learning_rate=0.001):
    device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
    model = model.to(device)
    optimizer = torch.optim.Adam(model.parameters(), lr=learning_rate)
    criterion = nn.CrossEntropyLoss()

    for epoch in range(num_epochs):
        model.train()
        total_loss = 0
        for batch_idx, (x, y) in enumerate(train_loader):
            x, y = x.to(device), y.to(device)
            optimizer.zero_grad()
            output = model(x)
            loss = criterion(output.view(-1, output.size(-1)), y.view(-1))
            loss.backward()
            optimizer.step()
            total_loss += loss.item()

            if batch_idx % 10 == 0:
                print(f"Epoch {epoch + 1}, Batch {batch_idx}, Loss: {loss.item():.4f}")

        avg_loss = total_loss / len(train_loader)
        print(f"Epoch {epoch + 1} completed, Average Loss: {avg_loss:.4f}")


# 5. 推理函数
def generate_text(model, processor, prompt, max_length=50):
    device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
    model.eval()

    # 编码输入提示
    input_ids = processor.encode(prompt)
    input_ids = torch.tensor(input_ids).unsqueeze(0).to(device)

    generated = []
    with torch.no_grad():
        for _ in range(max_length):
            outputs = model(input_ids)
            next_token_logits = outputs[:, -1, :]
            next_token = torch.argmax(next_token_logits, dim=-1)
            generated.append(next_token.item())
            input_ids = torch.cat([input_ids, next_token.unsqueeze(0)], dim=1)

            if next_token.item() == processor.word2idx["<EOS>"]:
                break

    return processor.decode(generated)


# 6. 主函数
def main():
    # 示例训练数据
    train_texts = [
        "你好 我 是 AI",
        "今天 天气 真 不错",
        "我 喜欢 学习 编程",
        # 添加更多训练数据...
    ]

    # 初始化文本处理器和词表
    processor = TextProcessor(vocab_size=1000)
    processor.build_vocab(train_texts)

    # 创建数据集和数据加载器
    dataset = SimpleDataset(train_texts, processor)
    train_loader = DataLoader(dataset, batch_size=2, shuffle=True)

    # 初始化模型
    model = SimpleLLM(
        vocab_size=processor.vocab_size,
        embedding_dim=256,
        hidden_dim=512,
        num_layers=1
    )

    # 训练模型
    train_model(model, train_loader, num_epochs=10)

    # 生成文本
    prompt = "你好 我"
    generated_text = generate_text(model, processor, prompt)
    print(f"输入: {prompt}")
    print(f"生成: {generated_text}")


if __name__ == "__main__":
    main()

运行起来之后,里面有的时候是 ”你好我是AI“ 有的时候是”你好我喜欢学习编程“

但是如果增大 epoch 次数,则更倾向于准确答案。

1.3.2 机器学习流程体现

各部分对应代码:

  1. 数据准备:

    • TextProcessor 类:构建词汇表 (build_vocab)、文本编码 (encode)

      SimpleDataset 类:将文本编码为索引并填充/截断到固定长度 (__init__ 方法)

      主函数中的 processor.build_vocab(train_texts) 和 dataset 创建

  1. 模型定义: SimpleLLM 类定义模型结构(嵌入层、自注意力层、前馈网络等)

  1. 损失函数选择: train_model 函数中的 criterion = nn.CrossEntropyLoss()

  1. 优化算法应用 :train_model 函数中的 optimizer = torch.optim.Adam(...)

  1. 训练循环: train_model 函数中的 for epoch in range(num_epochs): 循环和内部的数据加载循环

  1. 评估性能 :代码中未显式包含验证集评估,但 generate_text 函数可视为生成性能的定性评估。

1.3.3 LLM 概念体现

Token 和上下文窗口的体现:

  1. Token

• 定义:在 TextProcessor.encode() 中,通过 text.split() 将文本按空格分割为单词级别的 Token。

• 示例:句子 "今天 天气 不错" 会被分割为 ["今天", "天气", "不错"],再转换为索引如 [4, 5, 6]

  1. 上下文窗口

• 固定长度:在 SimpleDataset__init__ 方法中,所有文本被截断或填充到 seq_length(默认为 50)。

• 模型输入:在 __getitem__ 中,xencoded_text[:-1](前 49 个 Token),yencoded_text[1:](后 49 个 Token)。

• 注意力机制:SimpleLLM 中的 nn.MultiheadAttention 允许模型在训练时关注整个输入序列(长度 seq_length-1)的所有 Token。

1.4 LLM 前沿技术演进

1.4.1 微调

(待补充:微调原理)

1.4.2 多模态

(待补充:多模态原理)

1.4.2 深度思考 & 推理

(待补充:CoT 原理)

2. 单 LLM 应用开发实践

2.1 Chat Completion API

详情见:https://platform.openai.com/docs/api-reference/chat

2.1.1 Request 连接参数配置

在调用 ​OpenAI ChatCompletion API​(或兼容接口)时,无论使用哪种语言(Python/JS/Java等)或框架(LangChain/LangChain4j/SpringAI等),都必须配置以下 ​三个核心参数​ 以确保连接成功:

  • apiKey:

    • ​作用​:身份验证密钥,用于访问 OpenAI 或兼容 API 服务(如 Azure OpenAI、本地部署的 OpenAI 兼容模型)。

    • 示例值​:sk-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx

  • baseUrl:

    • API 服务的基础地址。默认 OpenAI 官方域名为 https://api.openai.com/v1

    • 若使用代理或自托管模型,需替换为对应地址(如 http://localhost:8000/v1)。

  • modelName:

    • ​作用​:指定调用的模型名称(如 gpt-3.5-turbogpt-4 或开源模型如 llama3-70b)。

以下为 LangChain4j 的示例:

public class Main {
    public static void main(String[] args) {
        String apiKey = "sk-xxxxxxxxxxxxxxxxxx";

        OpenAiChatModel model = OpenAiChatModel.builder()
                .baseUrl("https://api.hunyuan.cloud.tencent.com/v1")
                .apiKey(apiKey)
                .modelName("hunyuan-turbo")
                .build();

        String answer = model.chat("你好!你是谁?");
        System.out.println(answer); 
        // 我是腾讯开发的人工智能助手,可以帮你回答问题、提供建议,甚至讲笑话哦!
    }
}

2.1.2 Request 影响模型输出

一般来说,入参的请求体如下:

{
  "model": "gpt-4-turbo",
  "messages": [
    {
      "role": "system",
      "content": "你是一名资深技术顾问,用中文回答,回答需包含代码示例和详细解释。"
    },
    {
      "role": "user",
      "content": "请用Python演示如何调用OpenAI的ChatCompletion API,要求流式输出。"
    }
  ],
  "temperature": 0.7,
  "max_tokens": 1000,
  "top_p": 0.9,
  "frequency_penalty": 0.5,
  "presence_penalty": 0.3,
  "stream": true,
  "stop": ["\n\n", "###"],
  "response_format": {
    "type": "json_object"
  },
  "tools" :[]
}

  1. model

    • 指定使用的AI模型,比如这里用最新版的 gpt-4-turbo(性能更强,价格更便宜)

  2. messages

    • 对话消息列表,包含角色和内容:

      • system:系统指令(设定AI的角色和行为)

      • user:用户的提问或指令

      • (如果有多轮对话还会包含 assistant:AI之前的回复)

  1. temperature​ (0-2)

    • 控制回答的随机性:

      • 0 = 最确定/保守

      • 1 = 平衡(推荐)

      • 2 = 最大随机性

  2. max_tokens

    • 限制AI生成内容的最大长度(1个token≈0.75个英文单词)

  3. top_p(0-1)

    • 控制词汇选择的集中程度:

      • 0.9 = 只考虑概率最高的90%词汇

      • 与temperature二选一使用

  1. frequency_penalty (-2到2)

    • 惩罚重复用词:正值减少重复内容

  2. presence_penalty (-2到2)

    • 鼓励新话题:正值让AI更倾向提及新内容

  3. stream

    • 设为 true 时开启流式传输(数据会分块实时返回)

  4. stop

    • 遇到这些字符串时停止生成(比如设 ["。"] 会在句号处停止)

  5. response_format

    • 强制返回JSON格式(仅gpt-4-turbo支持)

  6. tools

    • 定义AI可以调用的外部工具,下面详细讲解

2.1.3 Response 直接输出

每一个框架都会封装以下 Response 以便于获取内容。

{
  "id": "resp_67ccd2bed1ec8190b14f964abc0542670bb6a6b452d3795b",
  "object": "response",
  "created_at": 1741476542,
  "status": "completed",
  "error": null,
  "incomplete_details": null,
  "instructions": null,
  "max_output_tokens": null,
  "model": "gpt-4.1-2025-04-14",
  "output": [
    {
      "type": "message",
      "id": "msg_67ccd2bf17f0819081ff3bb2cf6508e60bb6a6b452d3795b",
      "status": "completed",
      "role": "assistant",
      "content": [
        {
          "type": "output_text",
          "text": "In a peaceful grove beneath a silver moon, a unicorn named Lumina discovered a hidden pool that reflected the stars. As she dipped her horn into the water, the pool began to shimmer, revealing a pathway to a magical realm of endless night skies. Filled with wonder, Lumina whispered a wish for all who dream to find their own hidden magic, and as she glanced back, her hoofprints sparkled like stardust.",
          "annotations": []
        }
      ]
    }
  ],
  "parallel_tool_calls": true,
  "previous_response_id": null,
  "reasoning": {
    "effort": null,
    "summary": null
  },
  "store": true,
  "temperature": 1.0,
  "text": {
    "format": {
      "type": "text"
    }
  },
  "tool_choice": "auto",
  "tools": [],
  "top_p": 1.0,
  "truncation": "disabled",
  "usage": {
    "input_tokens": 36,
    "input_tokens_details": {
      "cached_tokens": 0
    },
    "output_tokens": 87,
    "output_tokens_details": {
      "reasoning_tokens": 0
    },
    "total_tokens": 123
  },
  "user": null,
  "metadata": {}
}

关注几个地方:

关于 output:

  • type: "message"
    表示这是一个完整的AI回复消息(区别于工具调用或其他类型响应)。

  • role: "assistant"
    标明这是AI(助手)的回复,与用户输入(
    user)或系统指令(system)区分。

  • content

    • type: "output_text":纯文本输出(如果是工具调用会是 tool_call)。

    • text:具体的生成内容(这里是关于独角兽的英文故事)。

    • annotations:附加标记(如引用来源、代码块等,这里为空)。

关于 token:

  • input_tokens(36)
    用户输入(
    messages中所有内容)消耗的token数,包括:

    • 系统指令 + 用户问题 + 历史对话(如果有)。

  • output_tokens​ (87)
    AI生成文本(
    output.text)消耗的token数。

  • total_tokens(123)
    本次请求总token数(
    input + output),直接影响API调用成本。

2.1.4 Response 流式输出

待补充:SSE / WebSocket 等

2.2 对话记忆管理

大型语言模型(LLMs)是无状态的,这意味着它们不会保留先前交互的信息。当希望在多个交互中保持上下文或状态时,这是一个限制。

USER: 你好!你是谁?
ASSISTANT: 你好!我是一个人工智能助手,旨在回答你的问题和提供帮助。如果你有任何问题或者需要讨论的内容,随时告诉我!

USER: 我是扎西拉姆
ASSISTANT: 你好,扎西拉姆!很高兴见到你。有什么我可以帮助你的吗?

USER: 我是谁?
ASSISTANT: 你是提问者,正在与我进行交流。如果你想分享更多关于自己的信息,或者有具体的问题,我很乐意帮助你!

USER: 我刚才说过了我是谁
ASSISTANT: 你好!如果你愿意,可以再告诉我一遍你是谁,或者分享一些你想讨论的内容。我在这里帮助你!

在选择记忆类型之前,理解聊天记忆和聊天历史之间的区别至关重要。

  • 聊天记忆。大型语言模型在整个对话过程中保留并使用以维持上下文感知的信息。

  • 聊天记录。整个对话历史,包括用户和模型之间交换的所有消息。

2.2.1 写入-对话历史的数据模型

对话历史该使用什么数据结构存储呢?这里给出一个参考的数据模型抽象:

  • Map<String, List<Message>>,即对话ID映射到消息列表;(也可以使用关系型数据库的主键索引实现,只要实现每个对话之间是隔离的就可以,注意并发问题)

  • 每条消息是一个 Message 对象,可以包含:

    • content:消息内容

    • messageType:消息类型(USER、ASSISTANT、SYSTEM、TOOL等)

    • metadata:消息元数据(时间戳、用户信息、模型信息、工具调用信息等)

    • media:可选,媒体内容数组

    • toolCalls/toolResponses:可选,工具调用或工具响应数组

{
  "user_123": [
    {
      "content": "你好",
      "messageType": "USER",
      "metadata": {
        "timestamp": "2024-01-15T10:30:00Z",
        "userId": "user_123",
        "clientType": "web",
        "sessionId": "session_abc123",
        "userAgent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7)",
        "ipAddress": "192.168.1.100",
        "language": "zh-CN"
      },
      "media": []
    },
    {
      "content": "你好!有什么可以帮助您的吗?",
      "messageType": "ASSISTANT",
      "metadata": {
        "timestamp": "2024-01-15T10:30:01Z",
        "modelName": "gpt-4o-mini",
        "responseTime": 850,
        "tokenUsage": {
          "promptTokens": 12,
          "completionTokens": 18,
          "totalTokens": 30
        },
        "temperature": 0.7,
        "maxTokens": 2048,
        "finishReason": "stop"
      },
      "toolCalls": [],
      "media": []
    },
    {
      "content": "介绍一下Spring AI",
      "messageType": "USER",
      "metadata": {
        "timestamp": "2024-01-15T10:31:15Z",
        "userId": "user_123",
        "clientType": "web",
        "sessionId": "session_abc123",
        "inputMethod": "keyboard",
        "messageLength": 11,
        "language": "zh-CN",
        "intent": "information_query"
      },
      "media": []
    },
    {
      "content": "Spring AI是一个强大的Java框架,专门用于构建AI驱动的应用程序。它提供了与各种大语言模型的集成能力,包括OpenAI、Azure OpenAI、Anthropic等,同时支持向量数据库、函数调用、RAG等高级功能...",
      "messageType": "ASSISTANT",
      "metadata": {
        "timestamp": "2024-01-15T10:31:18Z",
        "modelName": "gpt-4o-mini",
        "responseTime": 2340,
        "tokenUsage": {
          "promptTokens": 45,
          "completionTokens": 156,
          "totalTokens": 201
        },
        "temperature": 0.7,
        "maxTokens": 2048,
        "finishReason": "stop",
        "contentType": "informational",
        "topics": ["Spring AI", "Java", "LLM", "框架"]
      },
      "toolCalls": [],
      "media": []
    }
  ],
  "user_456": [
    {
      "content": "你是智能助手",
      "messageType": "SYSTEM",
      "metadata": {
        "timestamp": "2024-01-15T14:20:00Z",
        "systemPromptVersion": "v2.1",
        "role": "assistant",
        "capabilities": ["weather_query", "general_chat", "tool_calling"],
        "language": "zh-CN",
        "personality": "helpful_and_friendly"
      }
    },
    {
      "content": "帮我查询天气",
      "messageType": "USER",
      "metadata": {
        "timestamp": "2024-01-15T14:20:05Z",
        "userId": "user_456",
        "clientType": "mobile",
        "sessionId": "session_def456",
        "location": {
          "latitude": 39.9042,
          "longitude": 116.4074,
          "city": "北京",
          "country": "中国"
        },
        "userAgent": "Mozilla/5.0 (iPhone; CPU iPhone OS 17_0 like Mac OS X)",
        "language": "zh-CN",
        "intent": "weather_query"
      },
      "media": []
    },
    {
      "content": "正在查询天气信息,请稍等...",
      "messageType": "ASSISTANT",
      "metadata": {
        "timestamp": "2024-01-15T14:20:06Z",
        "modelName": "gpt-4o-mini",
        "responseTime": 650,
        "tokenUsage": {
          "promptTokens": 28,
          "completionTokens": 15,
          "totalTokens": 43
        },
        "temperature": 0.3,
        "toolCallPending": true,
        "nextAction": "weather_tool_call"
      },
      "toolCalls": [
        {
          "id": "call_weather_001",
          "name": "get_current_weather",
          "arguments": "{\"location\":\"北京\",\"unit\":\"celsius\"}",
          "metadata": {
            "callTime": "2024-01-15T14:20:06Z",
            "toolVersion": "v1.2",
            "priority": "high"
          }
        }
      ]
    },
    {
      "content": "天气查询工具返回结果",
      "messageType": "TOOL",
      "metadata": {
        "timestamp": "2024-01-15T14:20:08Z",
        "toolCallId": "call_weather_001",
        "toolName": "get_current_weather",
        "executionTime": 1200,
        "status": "success",
        "dataSource": "weather_api_v3"
      },
      "toolResponses": [
        {
          "id": "call_weather_001",
          "name": "get_current_weather",
          "content": "{\"location\":\"北京\",\"temperature\":25,\"condition\":\"晴朗\",\"humidity\":45,\"wind_speed\":\"5km/h\",\"forecast\":\"今日晴朗,适宜出行\"}",
          "metadata": {
            "responseTime": "2024-01-15T14:20:08Z",
            "dataFreshness": "real_time",
            "confidence": 0.95
          }
        }
      ]
    },
    {
      "content": "根据最新天气信息,北京今天天气晴朗,温度25°C,湿度45%,微风5km/h。今日天气很好,适宜出行!",
      "messageType": "ASSISTANT",
      "metadata": {
        "timestamp": "2024-01-15T14:20:10Z",
        "modelName": "gpt-4o-mini",
        "responseTime": 890,
        "tokenUsage": {
          "promptTokens": 85,
          "completionTokens": 42,
          "totalTokens": 127
        },
        "temperature": 0.3,
        "basedOnToolResult": true,
        "toolCallId": "call_weather_001",
        "contentType": "weather_report",
        "finishReason": "stop",
        "userSatisfaction": "pending"
      },
      "toolCalls": [],
      "media": []
    }
  ]
}

2.2.2 读取-聊天记忆上下文实现

我们在进行下一轮对话时,需要把上面已经存储的数据历史,先加工成上下文,再传入 Request。

我们需要考虑以下方面:

  • 自动加载:每次对话前自动加载历史记忆,并把当前对话进行合并

  • 自动保存:每次对话后自动保存新消息

  • 会话隔离:支持多用户、多会话并发(如 Java 的纯内存实现使用了并发集合)

  • 流式支持:完整支持流式对话记忆

  • 窗口化:最近N条数据管理,只保留最近 N 条数据传入上下文

具体实现上,每个框架略有不同,但几乎都考虑到了这些方面。

2.3 私域知识库与 RAG(待补充)

Milvus / ElasticSearch

2.3.1 写入-文档知识向量化

2.3.2 读取-检索增强生成 RAG

2.4 工具调用:Function Calling

如果我们不止想让大模型变成问答型,想让大模型变成“行动型”呢?

我们可以关注到大模型的 Request 里有这样一个参数 tools :

{
  "model": "gpt-4-turbo",
  "messages": [
    {
      "role": "system",
      "content": "你是一名资深技术顾问,用中文回答,回答需包含代码示例和详细解释。"
    },
    {
      "role": "user",
      "content": "请用Python演示如何调用OpenAI的ChatCompletion API,要求流式输出。"
    }
  ],
  // ....
  "tools" :[]
}

ChatGPT 的官方文档对 tools 定义如下:

目前 2025 年 7 月为止,Tools 的 type 仅支持 function,因此我们描述工具,就是描述 Function Call

举个例子,假如我们的请求体 Request 如下:

{
  "model": "gpt-4-0613",
  "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"]
        }
      }
    },
    {
      "type": "function",
      "function": {
        "name": "get_exchange_rate",
        "description": "查询两种货币之间的实时汇率",
        "parameters": {
          "type": "object",
          "properties": {
            "from": {
              "type": "string",
              "description": "源货币代码,如 USD"
            },
            "to": {
              "type": "string",
              "description": "目标货币代码,如 CNY"
            }
          },
          "required": ["from", "to"]
        }
      }
    }
  ]
}

这里有意思的事情发生了:以 OpenAI function calling(或类似大模型工具调用接口)为例,模型会根据你的输入和 tools 定义,自动决定是调用 get_weather、调用 get_exchange_rate,调用两者、还是直接回复而不调用任何 tool

不同情况,模型返回结构会有明显区别:

如果模型决定不调用任何 tools,内容和正常 content 相同:

{
  "role": "assistant",
  "content": "北京今天晴,最高气温25℃,最低气温14℃。",
  "tool_calls": null
}

如果模型决定只调用 get_weather 这个 tool,模型会返回一个 tool_calls 字段代替 content,内容如下:

{
  "role": "assistant",
  "content": null,
  "tool_calls": [
    {
      "id": "unique_call_id_1",
      "type": "function",
      "function": {
        "name": "get_weather",
        "arguments": "{\"city\": \"北京\", \"unit\": \"celsius\"}"
      }
    }
  ]
}

如果模型决定两个 tool 都调用,则可以返回多个 tool_call 以实现多函数调用:

{
  "role": "assistant",
  "content": null,
  "tool_calls": [
    {
      "id": "call_1",
      "type": "function",
      "function": {
        "name": "get_weather",
        "arguments": "{\"city\": \"北京\", \"unit\": \"celsius\"}"
      }
    },
    {
      "id": "call_2",
      "type": "function",
      "function": {
        "name": "get_exchange_rate",
        "arguments": "{\"from\": \"USD\", \"to\": \"CNY\"}"
      }
    }
  ]
}

我们可以发现 arguments 是标准化的,这样我们就可以在模型返回 tool_calls 字段时,业务代码直接调取相关函数 or 方法,执行相关业务逻辑了。

比如我们就可以编写 get_weather 和 get_exchange_rate 相关业务代码(Java/Python/Go/Js),对接天气 API 和汇率 API 接口,查询北京天气、USD 到 CNY 的汇率数据等,再返回给 AI。

假设你已完成了对 get_weather 和 get_exchange_rate 的 API 查询,获得了如下结果:

  • 北京天气:晴,25℃,湿度 60%

  • 美元兑人民币汇率:7.25

你可以将结果这样返回给大模型(content 字段自定,格式不限制):

[
  {
    "role": "function",
    "name": "get_weather",
    "content": "{\"city\": \"北京\", \"weather\": \"晴\", \"temperature\": 25, \"humidity\": 60}"
  },
  {
    "role": "function",
    "name": "get_exchange_rate",
    "content": "{\"from\": \"USD\", \"to\": \"CNY\", \"rate\": 7.25}"
  }
]

最后 AI 返回如下内容:

{
  "role": "assistant",
  "content": "北京今天晴,气温25℃,湿度60%。当前美元兑人民币汇率为7.25。"
}

Tools 让大模型不再只是“知识型AI”,而是“行动型AI”,能像助手一样帮用户完成实际任务。这是大模型走向智能体(Agent)和行业应用的关键能力之一。

3. 模块化 LLM 应用架构设计(Agent)

3.1 模块化设计

3.1.1 单一复杂组件的局限性

随着 LLM 驱动应用程序逻辑变得越来越复杂, 将其分解成更小的部分变得越来越重要,这也是软件开发中的常见做法。

  • Prompt:在系统提示中塞入大量指令以应对所有可能的场景, 容易出错且效率低下。如果指令太多,LLM 可能会忽略一些指令。 此外,指令呈现的顺序也很重要,这使得整个过程更具挑战性。

  • Function Calls:您的聊天机器人可能不需要始终了解您拥有的每一个工具。 例如,当用户只是向聊天机器人打招呼或说再见时, 让 LLM 访问数十或数百个工具既昂贵又有时甚至危险 (每个包含在 LLM 调用中的工具都会消耗大量的 token) 并可能导致意外结果(LLM 可能会产生幻觉或被操纵以使用非预期输入调用工具)。

  • RAG:有时需要向 LLM 提供一些上下文, 但并非总是如此,因为这会增加额外成本(更多上下文 = 更多 token) 并增加响应时间(更多上下文 = 更高延迟)。

  • 模型参数:在某些情况下,您可能需要 LLM 具有高度确定性, 因此您会设置较低的 temperature。在其他情况下,您可能会选择较高的 temperature,等等。

更小、更具体的组件更容易且更便宜地开发、测试、维护和理解。

让我们考虑一个简单的例子: 我想为我的公司构建一个聊天机器人。 如果用户向聊天机器人打招呼, 我希望它用预定义的问候语回应,而不依赖 LLM 生成问候语。 如果用户提出问题,我希望 LLM 使用公司的内部知识库生成回应(即 RAG)。

如果我们使用单一的 LLM 应用,流程图如下:

如果我们使用模块化的 LLM 应用,时序图如下:

3.1.2 Workflow 与 Agent

我们使用模块化的 LLM 应用时,注意到以上例子有一个路由控制器,需要判断调用哪个 LLM 节点。

考虑的方面涉及两个极端:

  • 是否希望您的应用程序高度确定性, 应用程序控制流程,而 LLM 只是组件之一?

    • 我们称这样的应用程序叫做 WorkFlow

  • 或者是否希望 LLM 拥有完全自主权并驱动您的应用程序?

    • 我们称这样的应用程序叫做 Agentic WorkFlow,简称 Agent

  • 或者根据情况混合使用两者?

当您将应用程序分解为更小、更易管理的部分时,所有这些选项都是可能的。

3.2 模块化实现范式分类

在LLM应用开发中,我们将系统的最小功能单元抽象为 ​Node(节点)​。每个Node封装了特定的能力(如调用LLM、执行工具、处理数据),而不同框架的核心差异在于如何编排这些Nodes以实现复杂逻辑。

下面介绍各个思想与框架,其中包含 LangChain、LangChain4j、LangGraph、Spring AI 等实现,它们的底层遵循的是范式也五花八门,大致可以有以下几种架构:

  • 链式架构(Chain-Based)​

    • 以 LangChain 为代表,通过线性串联组件实现工作流

    • 特点:简单直观,适合顺序性任务

  • 服务化架构(Service-Oriented)​

    • 以 LangChain4j AI Services 为代表

    • 特点:通过接口抽象隐藏复杂性,适合企业级开发

  • 状态图架构(StateGraph-Based)​

    • 以 LangGraph 为代表,通过节点/边/状态管理复杂逻辑

    • 特点:支持循环、分支和持久化,适合动态决策系统

  • Agent 架构(Agent-Based)​

    • 以 AutoGPT、ReAct、HuggingGPT 为代表,赋予LLM自主决策能力

    • 特点:可自主拆解任务并选择工具,根据执行结果实时修正策略

此外,还有类似 Spring AI(Java)、Eino(Golang,CloudWeGo开源)等综合开发框架,提供强大的流程“编排”,覆盖开发全流程。

3.2.1 链式架构

以 LangChain 为例(2023年初)

LangChain 中的 ​​"Chain"(链)​​ 这一命名源于其核心设计理念:​通过串联多个组件或步骤,构建模块化、可组合的工作流,类似于链条将各个环节连接起来。

LangChain 的核心思想是将语言模型(LLM)、提示模板(Prompt)、工具调用(Tools)、输出解析(Output Parsers)等组件像链条一样连接起来,形成有序的任务流程。例如:

# LCEL(LangChain表达式语言)示例:链式调用
chain = prompt_template | model | output_parser  # 类似Linux管道符"|"的串联逻辑[1,3](@ref)

这种设计允许开发者通过简单的语法将复杂任务分解为多个可复用的步骤。

每个 Chain 是一个独立模块,可通过组合实现更复杂的功能(如先检索文档再生成回答),避免重复造轮每个“链”是一个独立模块,可通过组合实现更复杂的功能(如先检索文档再生成回答),避免重复造轮子。

3.2.2 服务化架构

以 LangChain4j 的 AI Service 为例(2023年初)

在某些框架(如 LangChain4j)中,它们的开发者认为,Chains 组合多个低级组件并协调它们之间的交互,如果使用者需要自定义某些内容,它们过于僵化。

于是它们引入了一个新的概念:AI Services。其思想是将与 LLM 和其他组件交互的复杂性隐藏在简单的 API 后面。

这种方法非常适合 Java 的 SPI 机制:以声明方式定义具有所需 API 的接口, 然后 LangChain4j 提供实现该接口的对象(代理)。 可以将 AI 服务视为应用程序服务层中的组件。 它提供 AI 服务。

以下是如何将此任务分解为 2 个独立的 AI 服务(Java 代码表述):

interface GreetingExpert {

    @UserMessage("Is the following text a greeting? Text: {{it}}")
    boolean isGreeting(String text);
}

interface ChatBot {

    @SystemMessage("You are a polite chatbot of a company called Miles of Smiles.")
    String reply(String userMessage);
}

class MilesOfSmiles {

    private final GreetingExpert greetingExpert;
    private final ChatBot chatBot;
    
    ...
    
    public String handle(String userMessage) {
        if (greetingExpert.isGreeting(userMessage)) {
            return "Greetings from Miles of Smiles! How can I make your day better?";
        } else {
            return chatBot.reply(userMessage);
        }
    }
}

GreetingExpert greetingExpert = AiServices.create(GreetingExpert.class, llama2);

ChatBot chatBot = AiServices.builder(ChatBot.class)
    .chatLanguageModel(gpt4)
    .contentRetriever(milesOfSmilesContentRetriever)
    .build();

MilesOfSmiles milesOfSmiles = new MilesOfSmiles(greetingExpert, chatBot);

String greeting = milesOfSmiles.handle("Hello");
System.out.println(greeting); // Greetings from Miles of Smiles! How can I make your day better?

String answer = milesOfSmiles.handle("Which services do you provide?");
System.out.println(answer); // At Miles of Smiles, we provide a wide range of services ...

注意我们如何使用更便宜的 Llama2 来完成识别文本是否为问候语这一简单任务, 而使用更昂贵的 GPT-4 和内容检索器(RAG)来完成更复杂的任务。

这是一个非常简单且有些幼稚的例子,但希望它能说明这个想法。

现在,我们可以模拟 GreetingExpert 和 ChatBot,并单独测试 MilesOfSmiles。 还可以分别对 GreetingExpert 和 ChatBot 进行集成测试。 我们可以分别评估它们,并为每个子任务找到最优参数, 或者从长远来看,甚至可以为每个特定子任务微调一个小型专用模型。

3.2.3 状态图架构

以 LangGraph 为例(2024年)

采用 ​状态图(StateGraph)​​ 作为核心架构,通过节点(Node)、边(Edge)和状态(State)定义工作流,支持复杂逻辑的模块化设计

状态持久化​:均支持检查点(Checkpoint)机制,可保存和恢复执行状态,适用于中断恢复和长时任务

循环与分支​:支持条件边(Conditional Edges)和循环逻辑,突破传统链式调用的限制

3.2.4 Agent 架构

3.3 模块化通信协议

3.3.1 MCP

2024 年 11 月

3.3.2 A2A

2025 年

License:  CC BY 4.0