← 返回

Java 后端的 LLM 实战路线:微调、RAG、Agent 全链路拆解

📅 2026-05-28 | 🏷️ LLM, LoRA, RAG, Agent | 📖 阅读约 25 分钟


写作背景

这篇文章站在 Java 后端的视角写。我的切入点并非训练框架本身,重点是把模型能力接进已有系统:接口怎么设计,流式响应怎么落地,知识库怎么更新,工具调用怎么审计。Python 示例能解释原理,真正上线时还要回到鉴权、限流、日志、回滚和成本控制。 后端同学学习 LLM 时,可以先抓住工程边界:模型负责生成,应用负责上下文、权限、状态和证据。只要这个边界稳住,后续换模型、换向量库、换 Agent 框架都不会牵动整套业务。 Java 生态资料少,不代表 Java 不适合做 AI 应用。训练和实验可以交给 Python,在线服务、管理后台、任务调度、数据治理仍然是 Java 很熟悉的战场。 这篇文章按落地顺序展开:先会调用模型,再做 RAG,再理解微调和 Agent,末尾补评估、部署和安全。 数学细节可以以后补,工程上先把数据链路、接口契约和验证方法跑通。


一、全景图:LLM 落地的四层架构

flowchart TB App["应用层:对话、客服、知识库、文档助手"] Orchestration["编排层:工作流、工具路由、会话状态"] Capability["能力层:检索、重排、Embedding、评测"] Model["模型层:LLM API 或自部署推理服务"] Infra["基础设施:向量索引、业务库、对象存储、GPU"] App --> Orchestration Orchestration --> Capability Orchestration --> Model Capability --> Model Capability --> Infra Model --> Infra

Java 后端开发者的优势在应用层和编排层,你已经会 Spring Boot、会设计 API、会做系统集成。 需要补的是能力层(怎么用 RAG/微调提升效果)和基座层(怎么选模型、怎么部署)。


二、LoRA 微调:让模型学会你的业务

2.1 什么时候需要微调

判断是否微调时,先看问题来源。知识缺失优先走 RAG,格式稳定但表达不统一可以用提示词和模板,只有当模型缺少某类稳定能力或固定风格时,再考虑微调。 微调会带来数据准备、训练成本、版本管理和回归评估,不应当作为默认选项。

场景用 Prompt 工程用 RAG用微调
客服问答(基于文档)❌ 文档太长塞不进 prompt✅ 优先❌ 没必要
特定风格输出⚠️ few-shot 能凑合❌ 不相关✅ 优先
行业术语理解⚠️ 通用模型不太行❌ 不是检索问题✅ 优先
数据分析/报表✅ 写好 prompt 就行❌ 不需要❌ 杀鸡用牛刀
多轮对话记忆❌ 仅靠上下文窗口不够⚠️ 可结合历史检索/摘要做⚠️ 通常不靠微调直接解决

判断标准

  • 输出约束和少量示例可以解决的问题,先调整 Prompt
  • 缺少外部或时效知识时,评估 RAG
  • 知识充分但任务行为仍不稳定时,再准备训练集评估微调
  • 基座能力不足时,用固定评测集比较候选模型

2.2 LoRA 原理(Java 后端能懂的版本)

LoRA 可以理解为给基座模型增加一组很小的可训练适配层。上线时仍然加载原模型,再叠加这组适配层,因此训练成本和分发成本都比全量微调低很多。

LoRA 冻结基座权重 W,训练两个低秩矩阵 AB,用 ΔW = A × B 表示需要学习的增量,其中秩 r 小于原矩阵维度。训练参数量由目标层、秩、模型结构和精度共同决定,显存还受到优化器状态、激活、序列长度、批大小、量化和梯度检查点影响。不能只根据“7B”判断某张显卡一定可用。

部署时可以动态加载适配器,也可以在满足精度和回滚要求时合并权重。是否存在额外推理开销取决于运行方式和推理框架,应使用目标模型、真实上下文长度和并发量压测。

2.3 LoRA 超参数详解

实践里先关注五个参数:训练轮数控制学习次数,学习率控制更新幅度,rank 控制 LoRA 容量,batch size 控制吞吐与显存,cutoff length 控制单条样本长度。小数据集先用低学习率和少轮数试跑,再用固定评测集比较输出质量。

参数含义推荐值说明
r (rank)LoRA 矩阵的秩8-64越大学习能力越强,但容易过拟合
lora_alpha缩放系数通常 = 2r控制 LoRA 的影响力
lora_dropoutDropout 比例0.05-0.1防止过拟合
target_modules应用 LoRA 的层q_proj,v_proj一般只改注意力层
learning_rate学习率1e-4 ~ 3e-4比全量微调小很多
num_epochs训练轮数2-5数据少就少跑几轮
batch_size批大小看显存RTX 3090 24GB → batch_size=4
max_seq_length上限序列长度512-2048越长越吃显存

2.4 训练数据准备

训练数据决定微调上限。每条样本都要包含明确输入、期望输出和业务边界,脏样本会被模型认真学习。准备数据时先去重,再清理隐私字段,末尾抽样人工检查。

数据格式

// 对话格式(ChatML)
{
  "messages": [
    {"role": "system", "content": "你是一个专业的客服助手"},
    {"role": "user", "content": "你们的退货政策是什么?"},
    {"role": "assistant", "content": "我们支持 7 天无理由退货..."}
  ]
}

// 指令格式(Alpaca)
{
  "instruction": "将以下英文翻译成中文",
  "input": "Hello, how are you?",
  "output": "你好,你怎么样?"
}

数据量要求

评估不要只看训练日志。准备一组固定问题,覆盖正常输入、边界输入、拒答输入和格式要求,每次训练后用同一组问题回放,对比可读性、事实一致性和格式稳定性。

任务类型下限数据量推荐数据量说明
风格迁移100 条500-1000 条让模型学会某种说话风格
知识注入200 条1000-5000 条让模型理解特定领域术语
任务微调50 条200-500 条特定任务(如分类、提取)
对话微调500 条2000+ 条多轮对话能力

数据清洗 Checklist

常见坑有三类:数据里混入过期口径,模型学会了旧规则;样本格式不统一,输出开始飘;只看几个成功示例,没有覆盖失败场景。

□ 去重(重复数据会导致过拟合)
□ 去噪(删除乱码、不完整数据)
□ 统一格式(所有数据用同一种格式)
□ 检查长度(太短的删掉,太长的截断)
□ 质量抽检(随机抽 50 条人工检查)
□ 分布均衡(不要某一类数据占比太大)

2.5 微调工具链

工具可以分层选:训练用 LLaMA-Factory 或 Axolotl,推理用 vLLM 或 Ollama,评估用自建回放脚本,服务接入用 Spring AI 或轻量 HTTP Client。

方案一:LLaMA-Factory(推荐新手)

# 安装
git clone https://github.com/hiyouga/LLaMA-Factory.git
cd LLaMA-Factory
pip install -e .

# 准备数据(放到 data/ 目录)
# 编辑 data/dataset_info.json 注册数据集

# Web UI 训练(最简单)
llamafactory-cli webui

# 命令行训练
llamafactory-cli train \
    --model_name_or_path Qwen2.5-7B-Instruct \
    --dataset my_dataset \
    --template qwen \
    --finetuning_type lora \
    --lora_rank 16 \
    --lora_alpha 32 \
    --output_dir output/my_lora \
    --num_train_epochs 3 \
    --learning_rate 2e-4 \
    --per_device_train_batch_size 4 \
    --gradient_accumulation_steps 4

方案二:Hugging Face PEFT(更灵活)

from peft import LoraConfig, get_peft_model, TaskType
from transformers import AutoModelForCausalLM, AutoTokenizer, TrainingArguments, Trainer

# 加载基座模型
model = AutoModelForCausalLM.from_pretrained("Qwen/Qwen2.5-7B-Instruct")
tokenizer = AutoTokenizer.from_pretrained("Qwen/Qwen2.5-7B-Instruct")

# 配置 LoRA
lora_config = LoraConfig(
    task_type=TaskType.CAUSAL_LM,
    r=16,
    lora_alpha=32,
    lora_dropout=0.05,
    target_modules=["q_proj", "v_proj", "k_proj", "o_proj"],
)

# 应用 LoRA
model = get_peft_model(model, lora_config)
model.print_trainable_parameters()
# 输出: trainable params: 4,194,304 || all params: 7,615,616,000 || trainable%: 0.055%

# 训练
training_args = TrainingArguments(
    output_dir="./output",
    num_train_epochs=3,
    per_device_train_batch_size=4,
    gradient_accumulation_steps=4,
    learning_rate=2e-4,
    warmup_steps=100,
    logging_steps=10,
    save_steps=500,
    fp16=True,  # 混合精度训练
)

trainer = Trainer(
    model=model,
    args=training_args,
    train_dataset=train_dataset,
    tokenizer=tokenizer,
)
trainer.train()

# 保存 LoRA 权重(只有几十 MB)
model.save_pretrained("./output/my_lora")

方案三:云平台微调(不想管 GPU)

云 GPU 适合短期实验,本地显卡适合反复调试。选择时重点看显存、镜像可控性、数据上传限制和计费粒度。

平台优点缺点价格
阿里灵积国内稳定模型选择有限
百炼和千问配合好
Colab Pro算力充足需要科学上网$10/月
AutoDL国内 GPU 云要自己配环境

2.6 模型合并与部署

# LoRA 权重合并回基座模型
python scripts/merge_peft_adapter.py \
    --base_model Qwen/Qwen2.5-7B-Instruct \
    --peft_model output/my_lora \
    --output_path output/merged_model

# 合并后就是一个完整的模型,可以正常部署

部署可以分成三档:开发阶段用 Ollama 快速验证,内网服务用 vLLM 暴露 OpenAI 兼容接口,生产环境再加网关、鉴权、限流和灰度。

2.7 微调效果评估

效果评估要包含自动分和人工复核。自动分检查格式、关键词、长度和拒答规则,人工复核看事实是否稳、语气是否合适、是否产生越权回答。

评估维度方法指标
任务效果测试集准确率BLEU / ROUGE / 人工评分
灾难性遗忘对比微调前后通用能力通用 benchmark 分数
过拟合训练集 vs 验证集 loss 曲线loss 差距
推理速度合并后模型推理耗时tokens/s

三、RAG:让模型读你的文档

3.1 RAG 原理

flowchart LR subgraph Offline["离线摄取"] Doc["PDF、Word、Markdown"] --> Parse["解析与清洗"] Parse --> Chunk["结构化切片"] Chunk --> Embed["Embedding"] Embed --> Index["向量与关键词索引"] end subgraph Online["在线问答"] Q["用户问题"] --> Retrieve["混合检索"] Index --> Retrieve Retrieve --> Rerank["重排与权限过滤"] Rerank --> Generate["带证据生成"] Generate --> Answer["回答与引用"] end

3.2 文档处理 Pipeline

RAG 主链路是解析、切片、向量化、入库、检索、重排、生成。每一步都要保存版本号和处理状态,这样文档更新失败时可以重试,不会污染线上索引。

3.2.1 文档解析

// 文档解析器接口
public interface DocumentParser {
    List<DocumentChunk> parse(InputStream input, String filename);
}

// PDF 解析
public class PdfParser implements DocumentParser {
    // 方案一: Apache PDFBox(纯 Java,推荐)
    // 方案二: Tika(支持格式多,但重)
    // 方案三: 调 API(百度文档解析等)
}

// Word 解析
public class WordParser implements DocumentParser {
    // Apache POI
}

// Markdown 解析
public class MarkdownParser implements DocumentParser {
    // 按标题层级切分
}

3.2.2 文本切片(Chunking)

切片不要只按固定字数。标题、段落、表格、代码块要分开处理,切片里保留来源、章节、页码和更新时间。检索结果进入模型前,再按证据完整性和重复度做筛选。

public class TextChunker {

    // 方案一: 固定长度切片(最简单但最粗暴)
    public List<String> fixedSizeSplit(String text, int chunkSize, int overlap) {
        // chunkSize: 每片 500 字符
        // overlap: 前后重叠 50 字符(防止在句子中间切断)
    }

    // 方案二: 按段落/标题切分(推荐)
    public List<String> semanticSplit(String text) {
        // 按 \n\n 分段
        // 按 Markdown 标题 # ## ### 分节
        // 每段如果太长,再按句子细分
    }

    // 方案三: 递归切分(LangChain 的 RecursiveCharacterTextSplitter)
    public List<String> recursiveSplit(String text, int chunkSize) {
        // 先尝试按 \n\n 分
        // 如果某段太长,按 \n 分
        // 还是太长,按 。分
        // 还是太长,按字符硬切
    }
}

切片参数推荐

场景chunk_sizeoverlap说明
通用文档500 字符50 字符平衡效果和成本
技术文档800 字符100 字符代码块不能切断
法律合同1000 字符100 字符长句多,需要大窗口
FAQ每个 Q&A 一对0天然的切分单位

3.2.3 Embedding(向量化)

Embedding 选型先看语言、领域和成本。中文知识库要用中文表现稳定的模型,代码知识库要测试标识符和异常栈检索,跨语言场景要单独做评测集。

模型维度中文效果速度说明
text-embedding-ada-0021536⭐⭐⭐OpenAI,需 API
text-embedding-3-small1536⭐⭐⭐⭐OpenAI 新版
bge-large-zh-v1.51024⭐⭐⭐⭐⭐中文高强度,可本地部署
bge-m31024⭐⭐⭐⭐⭐多语言,支持稠密+稀疏
m3e-base768⭐⭐⭐⭐中文,轻量
nomic-embed-text768⭐⭐⭐Ollama 可直接用
// 调用 Embedding API
public interface EmbeddingService {
    float[] embed(String text);
    List<float[]> embedBatch(List<String> texts);
}

// OpenAI 实现
@Service
public class OpenAiEmbeddingService implements EmbeddingService {
    @Override
    public float[] embed(String text) {
        // 调 OpenAI /v1/embeddings API
    }
}

// 本地 BGE 模型实现(通过 Python 服务或 ONNX)
@Service
public class BgeEmbeddingService implements EmbeddingService {
    @Override
    public float[] embed(String text) {
        // 调本地 Python FastAPI 服务
        // 或用 ONNX Runtime 在 Java 中直接推理
    }
}

3.2.4 向量数据库选型

向量库选择不用追求功能堆满。小规模可以用 pgvector,便于和业务数据放在同一个运维体系;中大规模再看 Qdrant、Milvus 或 Elasticsearch 混合检索。

数据库类型Java SDK部署难度适合场景
Milvus专用向量库✅ 官方大规模(百万级以上)
Qdrant专用向量库✅ REST简单中小规模,功能全
Weaviate专用向量库✅ REST需要混合搜索
pgvectorPG 扩展✅ JDBC入门门槛低已有 PG,数据量不大
Elasticsearch搜索引擎✅ 官方已有 ES,顺便做向量
Redis缓存✅ Jedis简单小规模,要求快
Chroma专用向量库❌ REST入门门槛低原型验证

推荐方案

  • 已有 PostgreSQL → pgvector(零额外运维)
  • 需要大规模 → Milvus(分布式向量库,国内团队主导)
  • 快速原型 → Qdrant(单 binary 部署,REST API)
// pgvector 示例(JdbcTemplate)
@Service
public class VectorStoreService {

    @Autowired
    private JdbcTemplate jdbc;

    // 存储向量
    public void store(String docId, String content, float[] embedding) {
        jdbc.update(
            "INSERT INTO documents (doc_id, content, embedding) VALUES (?, ?, ?::vector)",
            docId, content, toPgVectorString(embedding)
        );
    }

    // 向量相似度搜索(余弦距离)
    public List<DocumentResult> search(float[] queryEmbedding, int topK) {
        return jdbc.query(
            "SELECT doc_id, content, 1 - (embedding <=> ?::vector) AS score " +
            "FROM documents ORDER BY embedding <=> ?::vector LIMIT ?",
            new Object[]{toPgVectorString(queryEmbedding), toPgVectorString(queryEmbedding), topK},
            (rs, i) -> new DocumentResult(
                rs.getString("doc_id"),
                rs.getString("content"),
                rs.getFloat("score")
            )
        );
    }
}

3.3 检索优化

RAG 进阶核心是评测闭环。每次调整切片、Embedding、召回数量、重排模型,都要在固定问题集上比较命中证据、答案准确率和响应耗时。

混合检索同时使用向量召回和关键词召回。前者处理语义相关问题,后者保留产品名、条款编号等精确词。两路结果可以用 Reciprocal Rank Fusion 合并,权重需要通过评测集调整。

3.3.2 Rerank(重排序)

重排模型对问题和候选片段重新打分,再按上下文预算选择证据。候选数量、保留数量和模型选择都属于评测参数,不能脱离语料、语言与延迟预算给出固定配置。自部署时可评估 BGE Reranker 等模型;托管服务还要核对数据出境、日志保留和调用成本。

3.3.3 Query 改写

Query 改写用于补全省略信息或拆分复合问题。以“你们支持退货吗”为例,改写器可以结合会话上下文生成“退货条件、办理流程、退款方式”等检索词。改写结果要和原问题一起参与召回,避免模型改写偏离用户意图。

flowchart LR Q["用户问题与会话上下文"] --> Rewrite["Query 改写"] Rewrite --> Vector["向量召回"] Rewrite --> Keyword["关键词召回"] Vector --> Fusion["RRF 融合去重"] Keyword --> Fusion Fusion --> Rerank["重排模型"] Rerank --> Select["按上下文预算选择证据"] Select --> Eval["引用校验与评测记录"]

3.4 RAG 完整架构(Java 后端版)

LLM、RAG、Agent 全链路架构图

@Service
public class RagService {

    @Autowired private DocumentParser documentParser;
    @Autowired private TextChunker textChunker;
    @Autowired private EmbeddingService embeddingService;
    @Autowired private VectorStoreService vectorStore;
    @Autowired private LlmService llmService;

    // 离线:文档入库
    public void ingestDocument(InputStream input, String filename) {
        List<DocumentChunk> chunks = documentParser.parse(input, filename);
        for (DocumentChunk chunk : chunks) {
            float[] embedding = embeddingService.embed(chunk.getContent());
            vectorStore.store(chunk.getId(), chunk.getContent(), embedding);
        }
    }

    // 在线:RAG 问答
    public String ask(String question) {
        // 1. 问题向量化
        float[] qEmbedding = embeddingService.embed(question);

        // 2. 检索相关片段
        List<DocumentResult> results = vectorStore.search(qEmbedding, 5);

        // 3. 构造 Prompt
        String context = results.stream()
            .map(DocumentResult::getContent)
            .collect(Collectors.joining("\n---\n"));

        String prompt = String.format("""
            基于以下参考资料回答用户问题。
            如果参考资料中没有相关信息,请说"我没有找到相关信息"。

            参考资料:
            %s

            用户问题:%s
            """, context, question);

        // 4. 调用 LLM 生成回答
        return llmService.chat(prompt);
    }
}

四、Agent / Skill:让模型调用工具

4.1 什么是 Agent

普通 LLM:只能聊天
RAG LLM:能基于文档回答问题
Agent LLM:能调用工具、执行操作

Agent = LLM(大脑) + Tools(手脚) + Memory(记忆) + Planning(规划)

4.2 Function Calling 原理

用户: "帮我查一下北京今天的天气"

LLM 判断需要调用工具:
→ 调用 get_weather(city="北京")
→ 拿到结果: {"temp": 25, "weather": "晴"}
→ 组织回答: "北京今天 25°C,天气晴朗"

4.3 Skill/Tool 定义(Java 版)

// Skill 接口定义
public interface Skill {
    String getName();
    String getDescription();
    List<ParameterSchema> getParameters();
    Object execute(Map<String, Object> params);
}

// 天气查询 Skill
@Component
public class WeatherSkill implements Skill {

    @Override
    public String getName() { return "get_weather"; }

    @Override
    public String getDescription() { return "查询指定城市的天气信息"; }

    @Override
    public List<ParameterSchema> getParameters() {
        return List.of(
            new ParameterSchema("city", "string", "城市名称", true)
        );
    }

    @Override
    public Object execute(Map<String, Object> params) {
        String city = (String) params.get("city");
        // 调天气 API
        return weatherApi.query(city);
    }
}

// 数据库查询 Skill
@Component
public class SqlQuerySkill implements Skill {

    @Override
    public String getName() { return "query_database"; }

    @Override
    public String getDescription() { return "查询数据库获取数据"; }

    @Override
    public List<ParameterSchema> getParameters() {
        return List.of(
            new ParameterSchema("sql", "string", "SQL 查询语句", true)
        );
    }

    @Override
    public Object execute(Map<String, Object> params) {
        String sql = (String) params.get("sql");
        // 只允许 SELECT,禁止修改数据
        if (!sql.trim().toUpperCase().startsWith("SELECT")) {
            throw new SecurityException("只允许查询操作");
        }
        return jdbcTemplate.queryForList(sql);
    }
}

4.4 Agent 编排引擎

@Service
public class AgentService {

    @Autowired
    private List<Skill> skills;

    @Autowired
    private LlmService llmService;

    public String run(String userMessage) {
        // 1. 把所有 Skill 定义转为 OpenAI function 格式
        List<FunctionDef> functions = skills.stream()
            .map(this::toFunctionDef)
            .toList();

        // 2. 第一次调用 LLM(判断是否需要调用工具)
        LlmResponse response = llmService.chat(userMessage, functions);

        // 3. 如果 LLM 决定调用工具
        if (response.hasToolCall()) {
            ToolCall toolCall = response.getToolCall();

            // 4. 执行工具
            Skill skill = findSkill(toolCall.getName());
            Object result = skill.execute(toolCall.getArguments());

            // 5. 把工具结果返回给 LLM,让它生成最终回答
            return llmService.chatWithToolResult(
                userMessage, toolCall, result
            );
        }

        // 6. 不需要工具,直接返回
        return response.getContent();
    }
}

4.5 常用 Skill 清单

Agent 适合有明确工具边界的任务,比如查库存、生成报表、创建工单、读取知识库。没有权限模型和审计日志的工具,不要直接暴露给 Agent。

Skill 名称功能实现方式安全风险
get_weather查天气调天气 API
query_database查数据库JDBC,只允许 SELECT中(SQL 注入)
send_email发邮件JavaMail中(滥用风险)
search_web搜索网页调搜索 API
read_file读文件文件 IO中(路径穿越)
call_api调外部 APIHTTP Client高(需白名单)
execute_code执行代码沙箱极高(需隔离)
generate_image生成图片调多模态 API
create_report生成报告模板 + 数据

4.6 Multi-Agent 架构

多 Agent 协作先从角色分工开始:一个负责规划,一个负责执行,一个负责校验。每个角色都要有输入输出契约,避免对话越滚越长。

多 Agent 只负责拆分上下文和职责,不能绕过权限体系。订单查询、退款、下单、日志查询等操作统一进入工具网关,由网关校验用户、租户、参数和审批状态。重启服务这类高风险动作不直接交给模型执行,应进入人工确认或受控自动化流程,并留下完整审计。


五、后端架构设计(Spring Boot)

5.1 整体模块划分

ai-platform/
├── ai-core/                    # 核心抽象层
│   ├── model/                  # 数据模型
│   │   ├── ChatMessage.java
│   │   ├── EmbeddingRequest.java
│   │   └── ToolCall.java
│   ├── service/                # 核心服务接口
│   │   ├── LlmService.java
│   │   ├── EmbeddingService.java
│   │   └── VectorStoreService.java
│   └── config/                 # 配置类
│       └── AiProperties.java
│
├── ai-llm/                     # LLM 对接层
│   ├── openai/                 # OpenAI 兼容接口
│   ├── qwen/                   # 千问
│   ├── deepseek/               # DeepSeek
│   └── ollama/                 # 本地模型
│
├── ai-rag/                     # RAG 模块
│   ├── parser/                 # 文档解析
│   ├── chunker/                # 文本切片
│   ├── embedding/              # Embedding 服务
│   ├── vectorstore/            # 向量存储
│   ├── retriever/              # 检索器
│   └── reranker/               # 重排序
│
├── ai-agent/                   # Agent 模块
│   ├── skill/                  # Skill 定义
│   ├── planner/                # 任务规划
│   ├── memory/                 # 记忆管理
│   └── executor/               # 执行器
│
├── ai-chat/                    # 对话模块
│   ├── controller/             # API 接口
│   ├── session/                # 会话管理
│   └── history/                # 历史记录
│
└── ai-admin/                   # 管理后台
    ├── model-manage/           # 模型管理
    ├── knowledge-manage/       # 知识库管理
    ├── prompt-manage/          # Prompt 管理
    └── monitor/                # 监控统计

5.2 LLM 统一调用接口

// 统一接口,屏蔽不同模型的差异
public interface LlmService {
    // 同步调用
    String chat(String systemPrompt, String userMessage);

    // 带工具调用
    LlmResponse chat(String userMessage, List<FunctionDef> functions);

    // 流式调用(SSE)
    Flux<String> chatStream(String systemPrompt, String userMessage);

    // 多轮对话
    String chat(List<ChatMessage> messages);
}

// 配置化:不同模型用不同实现
@Service
@ConditionalOnProperty(name = "ai.llm.provider", havingValue = "openai")
public class OpenAiLlmService implements LlmService { ... }

@Service
@ConditionalOnProperty(name = "ai.llm.provider", havingValue = "qwen")
public class QwenLlmService implements LlmService { ... }

5.3 流式输出(SSE)

@RestController
@RequestMapping("/api/chat")
public class ChatController {

    @Autowired
    private LlmService llmService;

    @GetMapping(value = "/stream", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
    public SseEmitter stream(@RequestParam String message) {
        SseEmitter emitter = new SseEmitter(60000L);

        Flux<String> stream = llmService.chatStream("你是助手", message);

        stream.subscribe(
            chunk -> emitter.send(SseEmitter.event().data(chunk)),
            emitter::completeWithError,
            emitter::complete
        );

        return emitter;
    }
}

5.4 会话管理

// 会话实体
@Data
@TableName("ai_chat_session")
public class ChatSession {
    @TableId(type = IdType.ASSIGN_ID)
    private Long sessionId;
    private Long userId;
    private String title;
    private String systemPrompt;
    private Integer messageCount;
    private LocalDateTime createTime;
    private LocalDateTime updateTime;
}

// 消息实体
@Data
@TableName("ai_chat_message")
public class ChatMessage {
    @TableId(type = IdType.ASSIGN_ID)
    private Long messageId;
    private Long sessionId;
    private String role;       // system/user/assistant/tool
    private String content;
    private String toolCalls;  // JSON
    private String toolResult; // JSON
    private Integer tokens;    // token 消耗
    private Long costMs;       // 耗时
    private LocalDateTime createTime;
}

5.5 成本控制

@Component
public class CostController {

    // Token 限流:每用户每天最多 N tokens
    private final Map<Long, AtomicLong> dailyTokenUsage = new ConcurrentHashMap<>();

    public void checkBudget(Long userId, int estimatedTokens) {
        long used = dailyTokenUsage.getOrDefault(userId, new AtomicLong(0)).get();
        long limit = getUserDailyLimit(userId);

        if (used + estimatedTokens > limit) {
            throw new BusinessException("今日 Token 额度已用完");
        }

        dailyTokenUsage.computeIfAbsent(userId, k -> new AtomicLong(0))
            .addAndGet(estimatedTokens);
    }
}

六、前端设计(Vue 3)

6.1 Chat UI 组件

组件可以拆成会话服务、模型网关、工具注册表、RAG 服务、任务队列和审计模块。业务系统只调用会话服务,不直接绑定具体模型供应商。

components/
├── ChatWindow.vue          # 主聊天窗口
├── ChatMessage.vue         # 单条消息渲染
├── ChatInput.vue           # 输入框(支持文件上传)
├── ChatSidebar.vue         # 会话列表
├── MarkdownRenderer.vue    # Markdown 渲染
├── CodeBlock.vue           # 代码块(带复制按钮)
├── StreamingText.vue       # 流式文本渲染
├── ToolCallCard.vue        # 工具调用展示卡片
└── KnowledgePanel.vue      # 知识库管理面板

6.2 流式消息渲染

<script setup lang="ts">
// SSE 流式接收
const sendMessage = async (content: string) => {
  const response = await fetch('/api/chat/stream?' + new URLSearchParams({ message: content }))
  const reader = response.body!.getReader()
  const decoder = new TextDecoder()

  while (true) {
    const { done, value } = await reader.read()
    if (done) break

    const chunk = decoder.decode(value)
    // SSE 格式解析
    const lines = chunk.split('\n')
    for (const line of lines) {
      if (line.startsWith('data:')) {
        const data = line.slice(5).trim()
        if (data === '[DONE]') break
        currentMessage.value += data  // 逐字追加
      }
    }

    // 自动滚动到底部
    scrollToBottom()
  }
}
</script>

6.3 知识库管理页面

知识库管理
├── 文档列表(上传/删除/重新解析)
├── 切片预览(查看每一片的内容和向量)
├── 测试检索(输入问题,查看检索结果)
├── 知识库统计(文档数、切片数、向量维度)
└── 配置管理(Embedding 模型、切片策略、检索参数)

七、部署架构

7.1 本地开发环境

flowchart LR App["Spring Boot 应用"] --> Ollama["Ollama 本地模型"] App --> PG["PostgreSQL 与 pgvector"] App --> Redis["Redis 会话缓存"]

7.2 生产部署

flowchart TB Client["Web 或 API 客户端"] --> Proxy["反向代理"] Proxy --> App["Spring Boot 应用集群"] App --> PG["PostgreSQL 与 pgvector"] App --> Redis["Redis"] App --> ModelGateway["模型网关"] ModelGateway --> Cloud["云端模型 API"] ModelGateway --> VLLM["vLLM 自部署"] ModelGateway --> Ollama["Ollama 开发或小规模部署"] App --> Metrics["指标与追踪"] ModelGateway --> Metrics

模型网关可以接入一种或多种后端,选择取决于数据边界、吞吐、成本和运维能力。图中分支表示候选后端,并非要求同时部署。数据库高可用、Redis 拓扑和应用实例数应按恢复目标与压测结果确定,不能用固定实例数替代容量设计。

7.3 GPU 资源评估

资源评估至少记录模型版本、权重精度、量化方式、上下文长度、KV Cache、并发请求、批处理策略、推理框架和目标首 token 延迟。训练还要加入优化器、梯度、激活与检查点开销。先在候选硬件上运行容量探测,再根据峰值显存保留故障与碎片余量。采购价格和云端单价变化快,应在决策时读取供应商报价,不写入长期文章。


八、学习路线图

学习路线建议按可交付物推进:第一阶段做流式聊天,第二阶段做小型知识库,第三阶段接入工具调用,第四阶段补评估和灰度,末尾再碰微调。

阶段一:基础(1-2 周)

□ 理解 Transformer 架构(不需要懂数学,理解注意力机制的概念)
□ 会用 OpenAI / 千问 API(Java 调用)
□ 理解 Prompt Engineering(系统提示、Few-shot、CoT)
□ 搭建一个简单的 Chat 应用(Spring Boot + Vue 3 + SSE)

阶段二:RAG(2-4 周)

□ 理解 Embedding 的概念
□ 会用向量数据库(pgvector 或 Qdrant)
□ 实现文档解析 → 切片 → Embedding → 存储 → 检索 → 生成
□ 优化检索效果(混合检索、Rerank)
□ 做一个"知识库问答"应用

阶段三:Agent(2-4 周)

□ 理解 Function Calling / Tool Use
□ 实现 Skill 接口 + Agent 编排
□ 做一个能调工具的 Chat 应用
□ 多 Agent 协作(可选)

阶段四:微调(2-4 周)

□ 准备训练数据(JSONL 格式)
□ 用 LLaMA-Factory 微调 7B 模型
□ 评估微调效果
□ 部署微调后的模型

阶段五:进阶(持续)

□ 模型评估与 A/B 测试
□ 多模态(图片理解、图片生成)
□ 语音交互(ASR + TTS)
□ 大规模知识库(千万级文档)
□ 安全与合规(内容审核、数据脱敏)

九、资源汇总

资源选择遵循一个原则:官方文档看接口和限制,开源项目看工程组织,论文看概念来源,自己的评测集看能否上线。

类别资源说明
入门OpenAI、Spring AI、模型厂商 API 文档先把调用、流式响应、错误处理跑通
RAGpgvector、Qdrant、RAGFlow、WeKnora 文档对照理解摄取、检索、重排和评测
微调LLaMA-Factory、Axolotl、vLLM先用合成样本试流程,再接真实脱敏数据
AgentOpenAI 工具调用、Spring AI Tool Calling重点看工具契约、权限和审计
Java 生态Spring AI、LangChain4j、Reactor、SSE负责在线服务和系统集成
社区开源项目 issue、技术博客、评测报告只吸收可复现的经验

本文是 Java 后端开发者学习 LLM 的实战路线,所有代码基于 Spring Boot 3.x + Java 17。持续更新中。