Java 后端的 LLM 实战路线:微调、RAG、Agent 全链路拆解
📅 2026-05-28 | 🏷️ LLM, LoRA, RAG, Agent | 📖 阅读约 25 分钟
写作背景
这篇文章站在 Java 后端的视角写。我的切入点并非训练框架本身,重点是把模型能力接进已有系统:接口怎么设计,流式响应怎么落地,知识库怎么更新,工具调用怎么审计。Python 示例能解释原理,真正上线时还要回到鉴权、限流、日志、回滚和成本控制。 后端同学学习 LLM 时,可以先抓住工程边界:模型负责生成,应用负责上下文、权限、状态和证据。只要这个边界稳住,后续换模型、换向量库、换 Agent 框架都不会牵动整套业务。 Java 生态资料少,不代表 Java 不适合做 AI 应用。训练和实验可以交给 Python,在线服务、管理后台、任务调度、数据治理仍然是 Java 很熟悉的战场。 这篇文章按落地顺序展开:先会调用模型,再做 RAG,再理解微调和 Agent,末尾补评估、部署和安全。 数学细节可以以后补,工程上先把数据链路、接口契约和验证方法跑通。
一、全景图:LLM 落地的四层架构
Java 后端开发者的优势在应用层和编排层,你已经会 Spring Boot、会设计 API、会做系统集成。 需要补的是能力层(怎么用 RAG/微调提升效果)和基座层(怎么选模型、怎么部署)。
二、LoRA 微调:让模型学会你的业务
2.1 什么时候需要微调
判断是否微调时,先看问题来源。知识缺失优先走 RAG,格式稳定但表达不统一可以用提示词和模板,只有当模型缺少某类稳定能力或固定风格时,再考虑微调。 微调会带来数据准备、训练成本、版本管理和回归评估,不应当作为默认选项。
| 场景 | 用 Prompt 工程 | 用 RAG | 用微调 |
|---|---|---|---|
| 客服问答(基于文档) | ❌ 文档太长塞不进 prompt | ✅ 优先 | ❌ 没必要 |
| 特定风格输出 | ⚠️ few-shot 能凑合 | ❌ 不相关 | ✅ 优先 |
| 行业术语理解 | ⚠️ 通用模型不太行 | ❌ 不是检索问题 | ✅ 优先 |
| 数据分析/报表 | ✅ 写好 prompt 就行 | ❌ 不需要 | ❌ 杀鸡用牛刀 |
| 多轮对话记忆 | ❌ 仅靠上下文窗口不够 | ⚠️ 可结合历史检索/摘要做 | ⚠️ 通常不靠微调直接解决 |
判断标准:
- 输出约束和少量示例可以解决的问题,先调整 Prompt
- 缺少外部或时效知识时,评估 RAG
- 知识充分但任务行为仍不稳定时,再准备训练集评估微调
- 基座能力不足时,用固定评测集比较候选模型
2.2 LoRA 原理(Java 后端能懂的版本)
LoRA 可以理解为给基座模型增加一组很小的可训练适配层。上线时仍然加载原模型,再叠加这组适配层,因此训练成本和分发成本都比全量微调低很多。
LoRA 冻结基座权重 W,训练两个低秩矩阵 A、B,用 ΔW = A × B 表示需要学习的增量,其中秩 r 小于原矩阵维度。训练参数量由目标层、秩、模型结构和精度共同决定,显存还受到优化器状态、激活、序列长度、批大小、量化和梯度检查点影响。不能只根据“7B”判断某张显卡一定可用。
部署时可以动态加载适配器,也可以在满足精度和回滚要求时合并权重。是否存在额外推理开销取决于运行方式和推理框架,应使用目标模型、真实上下文长度和并发量压测。
2.3 LoRA 超参数详解
实践里先关注五个参数:训练轮数控制学习次数,学习率控制更新幅度,rank 控制 LoRA 容量,batch size 控制吞吐与显存,cutoff length 控制单条样本长度。小数据集先用低学习率和少轮数试跑,再用固定评测集比较输出质量。
| 参数 | 含义 | 推荐值 | 说明 |
|---|---|---|---|
r (rank) | LoRA 矩阵的秩 | 8-64 | 越大学习能力越强,但容易过拟合 |
lora_alpha | 缩放系数 | 通常 = 2r | 控制 LoRA 的影响力 |
lora_dropout | Dropout 比例 | 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 原理
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_size | overlap | 说明 |
|---|---|---|---|
| 通用文档 | 500 字符 | 50 字符 | 平衡效果和成本 |
| 技术文档 | 800 字符 | 100 字符 | 代码块不能切断 |
| 法律合同 | 1000 字符 | 100 字符 | 长句多,需要大窗口 |
| FAQ | 每个 Q&A 一对 | 0 | 天然的切分单位 |
3.2.3 Embedding(向量化)
Embedding 选型先看语言、领域和成本。中文知识库要用中文表现稳定的模型,代码知识库要测试标识符和异常栈检索,跨语言场景要单独做评测集。
| 模型 | 维度 | 中文效果 | 速度 | 说明 |
|---|---|---|---|---|
text-embedding-ada-002 | 1536 | ⭐⭐⭐ | 快 | OpenAI,需 API |
text-embedding-3-small | 1536 | ⭐⭐⭐⭐ | 快 | OpenAI 新版 |
bge-large-zh-v1.5 | 1024 | ⭐⭐⭐⭐⭐ | 中 | 中文高强度,可本地部署 |
bge-m3 | 1024 | ⭐⭐⭐⭐⭐ | 中 | 多语言,支持稠密+稀疏 |
m3e-base | 768 | ⭐⭐⭐⭐ | 快 | 中文,轻量 |
nomic-embed-text | 768 | ⭐⭐⭐ | 快 | 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 | 中 | 需要混合搜索 |
| pgvector | PG 扩展 | ✅ 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、召回数量、重排模型,都要在固定问题集上比较命中证据、答案准确率和响应耗时。
3.3.1 混合检索(Hybrid Search)
混合检索同时使用向量召回和关键词召回。前者处理语义相关问题,后者保留产品名、条款编号等精确词。两路结果可以用 Reciprocal Rank Fusion 合并,权重需要通过评测集调整。
3.3.2 Rerank(重排序)
重排模型对问题和候选片段重新打分,再按上下文预算选择证据。候选数量、保留数量和模型选择都属于评测参数,不能脱离语料、语言与延迟预算给出固定配置。自部署时可评估 BGE Reranker 等模型;托管服务还要核对数据出境、日志保留和调用成本。
3.3.3 Query 改写
Query 改写用于补全省略信息或拆分复合问题。以“你们支持退货吗”为例,改写器可以结合会话上下文生成“退货条件、办理流程、退款方式”等检索词。改写结果要和原问题一起参与召回,避免模型改写偏离用户意图。
3.4 RAG 完整架构(Java 后端版)

@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 | 调外部 API | HTTP 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 本地开发环境
7.2 生产部署
模型网关可以接入一种或多种后端,选择取决于数据边界、吞吐、成本和运维能力。图中分支表示候选后端,并非要求同时部署。数据库高可用、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 文档 | 先把调用、流式响应、错误处理跑通 |
| RAG | pgvector、Qdrant、RAGFlow、WeKnora 文档 | 对照理解摄取、检索、重排和评测 |
| 微调 | LLaMA-Factory、Axolotl、vLLM | 先用合成样本试流程,再接真实脱敏数据 |
| Agent | OpenAI 工具调用、Spring AI Tool Calling | 重点看工具契约、权限和审计 |
| Java 生态 | Spring AI、LangChain4j、Reactor、SSE | 负责在线服务和系统集成 |
| 社区 | 开源项目 issue、技术博客、评测报告 | 只吸收可复现的经验 |
本文是 Java 后端开发者学习 LLM 的实战路线,所有代码基于 Spring Boot 3.x + Java 17。持续更新中。