第2192篇:LLM工程技术栈全景——2024年企业AI选型的底层逻辑
第2192篇:LLM工程技术栈全景——2024年企业AI选型的底层逻辑
适读人群:需要系统构建AI工程能力的技术负责人和架构师 | 阅读时长:约20分钟 | 核心价值:从实战出发梳理LLM工程技术栈的五个层次,给出有观点、有取舍依据的选型建议
半年前,我们公司召开了一次AI技术选型会。
会议室里贴了一张A0纸,上面密密麻麻写了三十多个工具和框架的名字:LangChain、LlamaIndex、Spring AI、LangChain4j、Pinecone、Milvus、Weaviate、Qdrant、Chroma、MLflow、Weights & Biases、Phoenix、LangFuse、PromptFlow……
看着这张纸,CTO说了一句话:"我现在不担心我们没工具可用,我担心的是我们不知道选哪个,最后每个人选一套,最后谁也维护不了。"
这句话道出了2024年AI工程的最大困境:不是技术不够用,而是选择太多,缺乏有依据的取舍框架。
我花了大概三个月,把公司用过的、同行用过的、自己踩过坑的工具梳理了一遍,试图给出一个相对有说服力的选型全景。不保证这是最优答案——技术在快速演进,六个月后可能有部分已经过时。但选型的思维框架应该是稳定的。
LLM工程技术栈的五个层次
在具体工具之前,先说结构。
LLM工程技术栈从下往上可以分五层:
┌─────────────────────────────────────────────┐
│ Layer 5: 运维与治理层 │
│ 监控/告警/成本/安全/审计 │
├─────────────────────────────────────────────┤
│ Layer 4: 评估与实验层 │
│ 测试/评估/Prompt实验/A-B测试 │
├─────────────────────────────────────────────┤
│ Layer 3: 存储与检索层 │
│ 向量数据库/文档存储/缓存/知识库 │
├─────────────────────────────────────────────┤
│ Layer 2: 编排与应用层 │
│ LLM框架/RAG/Agent/工作流编排 │
├─────────────────────────────────────────────┤
│ Layer 1: 模型接入层 │
│ LLM API/多模型路由/本地模型推理 │
└─────────────────────────────────────────────┘这五层缺一不可。绝大多数团队在Layer 1和Layer 2投入了大量精力,但Layer 4和Layer 5几乎是空白——没有评估体系,没有治理能力。上线之后"感觉还行",出了问题又完全无从定位。
下面逐层分析。
Layer 1:模型接入层
这一层最容易被低估。
很多团队的模型接入是这样的:直接调用OpenAI SDK,key硬编码在配置文件里,没有重试,没有failover,没有成本追踪。这在Demo阶段没问题,在生产阶段迟早要爆。
Java生态的选择
Java生态主要有两个选择:
Spring AI
- 优点:与Spring生态深度整合,自动配置,学习曲线平缓,适合大型企业项目
- 缺点:相比Python生态,功能更新相对滞后;高级特性如Agent、复杂工作流支持有限
- 适合场景:企业级Spring Boot项目,需要稳定可靠的LLM集成
LangChain4j
- 优点:功能更丰富,Agent/工具调用支持更成熟,更接近Python LangChain的能力
- 缺点:社区相对较小,稳定性不如Spring AI
- 适合场景:需要构建复杂Agent系统或多步骤推理工作流的项目
我的实践结论:如果是企业内部的业务AI功能,选Spring AI。如果是构建AI产品或复杂Agent系统,选LangChain4j。
/**
* 企业级LLM客户端封装
*
* 这一层做了什么:
* 1. 多Provider支持(OpenAI / 通义千问 / 文心一言 / 本地模型)
* 2. 自动重试(指数退避)
* 3. Failover(主模型挂了自动切到备用)
* 4. 成本追踪(每次调用记录token消耗)
* 5. 延迟监控(P95/P99延迟告警)
*/
@Service
@Slf4j
public class ProductionLLMClient {
private final List<LLMProvider> providers; // 按优先级排序
private final RetryTemplate retryTemplate;
private final MeterRegistry meterRegistry;
private final LLMCostTracker costTracker;
/**
* 带完整生产保障的同步调用
*/
public LLMResponse call(LLMRequest request) {
String traceId = UUID.randomUUID().toString();
long startTime = System.currentTimeMillis();
for (int providerIdx = 0; providerIdx < providers.size(); providerIdx++) {
LLMProvider provider = providers.get(providerIdx);
try {
LLMResponse response = retryTemplate.execute(context -> {
return provider.call(request);
});
long latencyMs = System.currentTimeMillis() - startTime;
// 记录成功指标
meterRegistry.counter("llm.calls",
"provider", provider.getName(),
"status", "success").increment();
meterRegistry.timer("llm.latency",
"provider", provider.getName())
.record(latencyMs, TimeUnit.MILLISECONDS);
// 异步记录成本
costTracker.recordAsync(CostRecord.builder()
.traceId(traceId)
.provider(provider.getName())
.inputTokens(response.getInputTokens())
.outputTokens(response.getOutputTokens())
.latencyMs(latencyMs)
.build());
if (providerIdx > 0) {
log.warn("Failover成功: 切换到第{}个Provider, traceId={}",
providerIdx + 1, traceId);
}
return response;
} catch (Exception e) {
log.error("Provider调用失败: provider={}, error={}, traceId={}",
provider.getName(), e.getMessage(), traceId);
meterRegistry.counter("llm.calls",
"provider", provider.getName(),
"status", "failure").increment();
if (providerIdx == providers.size() - 1) {
// 所有Provider都失败了
throw new AllProvidersFailedException(
"所有LLM Provider均不可用", e);
}
// 继续尝试下一个Provider
}
}
throw new IllegalStateException("不应该到达这里");
}
}多Provider路由策略
单纯的Failover只能处理故障场景,生产中更常见的需求是按业务场景路由到不同模型:
/**
* 基于业务场景的模型路由
*
* 核心思路:不同任务对模型的要求不同
* - 简单分类/提取:用便宜快速的小模型
* - 复杂推理/生成:用强大的大模型
* - 实时对话:用延迟低的模型
* - 批量处理:用成本低的模型
*/
@Component
public class SceneBasedModelRouter {
@Value("${llm.routing.rules:}")
private String routingRulesConfig;
private final Map<SceneType, ModelConfig> routingTable = new EnumMap<>(SceneType.class);
@PostConstruct
public void initRoutingTable() {
// 可以从配置文件动态加载,支持不重启切换模型
routingTable.put(SceneType.SIMPLE_EXTRACTION,
ModelConfig.of("gpt-3.5-turbo", 0.0, 512));
routingTable.put(SceneType.COMPLEX_REASONING,
ModelConfig.of("gpt-4o", 0.0, 4096));
routingTable.put(SceneType.REALTIME_CHAT,
ModelConfig.of("gpt-4o-mini", 0.7, 1024));
routingTable.put(SceneType.BATCH_PROCESSING,
ModelConfig.of("qwen-turbo", 0.0, 2048)); // 成本更低
routingTable.put(SceneType.CODE_GENERATION,
ModelConfig.of("deepseek-coder", 0.0, 8192));
}
public ModelConfig route(LLMRequest request) {
SceneType scene = request.getSceneType();
ModelConfig config = routingTable.getOrDefault(scene,
routingTable.get(SceneType.COMPLEX_REASONING));
log.debug("模型路由: scene={} -> model={}", scene, config.getModelId());
return config;
}
}Layer 2:编排与应用层
这一层是大多数AI工程师花时间最多的地方——RAG、Agent、工作流、多步推理。
RAG的工程现实
RAG(检索增强生成)是2024年最成熟的AI应用模式,但也是踩坑最多的地方。
理论上的RAG很简单:把文档切片、向量化,检索时查相似片段,拼到Prompt里让模型回答。
实际工程中,这套逻辑在"能跑通"和"真好用"之间有巨大的鸿沟:
/**
* 生产级RAG实现
*
* 对比简单RAG的关键改进点:
* 1. 混合检索(向量 + 关键词BM25)而不是纯向量
* 2. 重排序(Reranker)过滤掉语义相似但实际无关的结果
* 3. 查询改写(解决用户问题描述不准确的问题)
* 4. 上下文压缩(去掉检索结果里的无关内容)
*/
@Service
@Slf4j
public class ProductionRAGService {
private final VectorStore vectorStore;
private final BM25Searcher bm25Searcher;
private final QueryRewriter queryRewriter;
private final CrossEncoderReranker reranker;
private final ContextCompressor contextCompressor;
private final LLMClient llmClient;
public RAGResponse query(String userQuestion, String knowledgeBaseId) {
// Step 1: 查询改写
// 用户问"这个功能怎么用",改写成"[功能名称]的使用方法和步骤"
String rewrittenQuery = queryRewriter.rewrite(userQuestion);
log.debug("查询改写: original='{}' -> rewritten='{}'",
userQuestion, rewrittenQuery);
// Step 2: 混合检索
// 向量检索捕捉语义相关性,BM25检索捕捉关键词匹配
List<Document> vectorResults = vectorStore.similaritySearch(
rewrittenQuery, knowledgeBaseId, 20); // 多取一些,后面要rerank
List<Document> keywordResults = bm25Searcher.search(
rewrittenQuery, knowledgeBaseId, 10);
// 去重合并
List<Document> candidates = mergeDeduplicate(vectorResults, keywordResults);
// Step 3: 重排序
// CrossEncoder比Bi-Encoder准确,但慢,所以只在top-20结果上跑
List<ScoredDocument> rerankedDocs = reranker.rerank(
rewrittenQuery, candidates, 5); // 最终保留5个
if (rerankedDocs.isEmpty()) {
return RAGResponse.noContext(userQuestion);
}
// Step 4: 上下文压缩
// 把5个文档里和问题无关的段落删掉,减少噪音
String compressedContext = contextCompressor.compress(
rewrittenQuery,
rerankedDocs.stream().map(ScoredDocument::getDocument)
.collect(Collectors.toList()));
// Step 5: 生成答案(带来源引用)
String prompt = buildPromptWithSources(userQuestion, compressedContext, rerankedDocs);
LLMResponse llmResponse = llmClient.call(LLMRequest.of(prompt));
return RAGResponse.builder()
.answer(llmResponse.getContent())
.sources(rerankedDocs.stream()
.map(ScoredDocument::getSourceInfo)
.collect(Collectors.toList()))
.rewrittenQuery(rewrittenQuery)
.build();
}
private String buildPromptWithSources(
String question, String context, List<ScoredDocument> sources) {
StringBuilder sb = new StringBuilder();
sb.append("你是一个专业的知识库助手。请根据以下参考资料回答用户的问题。\n\n");
sb.append("【参考资料】\n").append(context).append("\n\n");
sb.append("【用户问题】\n").append(question).append("\n\n");
sb.append("回答要求:\n");
sb.append("1. 只使用参考资料中的信息,不要编造\n");
sb.append("2. 如果参考资料不足以回答问题,明确说明\n");
sb.append("3. 在答案末尾注明引用了哪些资料\n");
return sb.toString();
}
}Agent的工程化困境
Agent是2024年最热的方向,也是工程化最难的地方。
我的观察是:Agent适合的场景远比大家以为的要窄。
以下情况不适合用Agent:
- 流程完全固定的任务(用编排工作流更稳定)
- 对准确性要求极高的场景(Agent决策链路长,出错概率叠加)
- 需要实时响应的场景(Agent多步调用延迟高)
以下情况适合用Agent:
- 任务步骤不确定,需要根据中间结果决定下一步
- 工具集合丰富,需要灵活组合
- 对延迟容忍度高(比如后台异步任务)
Layer 3:存储与检索层
向量数据库的选型是一个被过度讨论、被欠度实践的话题。
向量数据库选型的实用原则
我见过很多团队花了大量时间比较不同向量数据库的benchmark,最后发现这些benchmark跟他们的实际使用场景相差甚远。
真正影响选型的因素(按重要性排序):
- 现有基础设施是否兼容:用PostgreSQL就用pgvector,用Elasticsearch就用它的向量检索扩展,不需要引入新组件
- 数据规模:100万以下文档,Chroma或内存版Milvus完全够用
- 运维能力:团队有没有人专门运维向量数据库
- 混合检索需求:需要向量+BM25混合检索的,Elasticsearch/OpenSearch更合适
/**
* 向量数据库抽象层
*
* 关键设计:上层代码不直接依赖具体的向量数据库实现
* 这样切换时只需换一个实现类
*/
public interface VectorRepository {
void upsert(String collectionName, List<VectorDocument> documents);
List<ScoredDocument> search(
String collectionName,
float[] queryVector,
int topK,
Map<String, Object> filter);
void delete(String collectionName, List<String> ids);
long count(String collectionName);
}
/**
* Milvus实现(适合百万级以上大规模场景)
*/
@Component
@ConditionalOnProperty(name = "vector.store.type", havingValue = "milvus")
public class MilvusVectorRepository implements VectorRepository {
private final MilvusServiceClient milvusClient;
@Override
public List<ScoredDocument> search(
String collectionName,
float[] queryVector,
int topK,
Map<String, Object> filter) {
SearchParam searchParam = SearchParam.newBuilder()
.withCollectionName(collectionName)
.withMetricType(MetricType.COSINE)
.withTopK(topK)
.withVectors(List.of(toFloatList(queryVector)))
.withVectorFieldName("embedding")
.withParams("{\"ef\": 64}") // HNSW参数,影响召回率
.withExpr(buildFilterExpr(filter))
.build();
R<SearchResults> response = milvusClient.search(searchParam);
if (response.getStatus() != R.Status.Success.getCode()) {
throw new VectorSearchException("Milvus搜索失败: " + response.getMessage());
}
return parseSearchResults(response.getData());
}
private String buildFilterExpr(Map<String, Object> filter) {
if (filter == null || filter.isEmpty()) return "";
return filter.entrySet().stream()
.map(e -> String.format("%s == \"%s\"", e.getKey(), e.getValue()))
.collect(Collectors.joining(" && "));
}
}语义缓存——被低估的优化手段
向量数据库不只用来存知识库,还可以用来做语义缓存:把已经回答过的问题和答案缓存起来,当新问题与历史问题语义相似时,直接返回缓存答案,不再调用LLM。
/**
* 语义缓存
*
* 实测效果:
* - 对重复率高的场景(如FAQ问答)可以减少60-80%的LLM调用
* - 延迟从1-3秒降到10-50ms
* - 成本显著降低
*
* 注意事项:
* - 相似度阈值要调高(0.95以上),避免返回不准确的缓存
* - 缓存需要有TTL,知识库更新后缓存要失效
*/
@Service
public class SemanticCacheService {
private final VectorStore cacheStore;
private final EmbeddingModel embeddingModel;
private static final float SIMILARITY_THRESHOLD = 0.95f;
private static final Duration CACHE_TTL = Duration.ofHours(24);
public Optional<CachedAnswer> lookup(String question) {
float[] questionEmbedding = embeddingModel.embed(question);
List<ScoredDocument> results = cacheStore.similaritySearch(
questionEmbedding, "semantic_cache", 1,
Map.of("expires_after", Instant.now().toString()));
if (results.isEmpty()) return Optional.empty();
ScoredDocument top = results.get(0);
if (top.getScore() < SIMILARITY_THRESHOLD) {
return Optional.empty();
}
log.debug("语义缓存命中: question='{}', similarity={:.3f}",
question, top.getScore());
return Optional.of(CachedAnswer.from(top.getDocument()));
}
public void store(String question, String answer, Map<String, Object> metadata) {
float[] questionEmbedding = embeddingModel.embed(question);
CacheDocument doc = CacheDocument.builder()
.id(UUID.randomUUID().toString())
.question(question)
.answer(answer)
.embedding(questionEmbedding)
.createdAt(Instant.now())
.expiresAt(Instant.now().plus(CACHE_TTL))
.metadata(metadata)
.build();
cacheStore.upsert("semantic_cache", List.of(doc));
}
}Layer 4:评估与实验层
这是大多数团队最薄弱的一层,也是最重要的一层。
没有评估体系,你不知道:
- Prompt改了之后是变好了还是变差了
- 切换模型之后质量变化了多少
- 哪类问题的回答质量最差
有了评估体系,AI系统才从"黑盒靠感觉"变成"可量化可改进"。
评估框架设计
/**
* LLM应用评估框架
*
* 三种评估方式,适合不同场景:
* 1. 确定性评估:有标准答案,直接比对
* 2. LLM评估:让另一个LLM打分(成本高但灵活)
* 3. 人工评估:采样后人工打标(准确但慢)
*/
@Service
@Slf4j
public class LLMEvaluationFramework {
private final LLMClient evaluatorLLM; // 用于LLM评估的模型(通常比被评估模型更强)
private final EvaluationDatasetRepository datasetRepo;
private final EvaluationResultRepository resultRepo;
/**
* 运行完整评估
*
* @param systemUnderTest 被评估的系统
* @param datasetId 评估数据集ID
* @param evaluationType 评估类型
*/
public EvaluationReport runEvaluation(
LLMSystem systemUnderTest,
String datasetId,
EvaluationType evaluationType) {
List<EvaluationCase> cases = datasetRepo.load(datasetId);
log.info("开始评估: dataset={}, cases={}, type={}",
datasetId, cases.size(), evaluationType);
List<CaseResult> results = cases.parallelStream()
.map(testCase -> evaluateSingle(systemUnderTest, testCase, evaluationType))
.collect(Collectors.toList());
EvaluationReport report = EvaluationReport.builder()
.datasetId(datasetId)
.totalCases(cases.size())
.passRate(calculatePassRate(results))
.averageScore(calculateAverageScore(results))
.failedCases(results.stream()
.filter(r -> !r.isPassed())
.collect(Collectors.toList()))
.evaluatedAt(Instant.now())
.build();
resultRepo.save(report);
log.info("评估完成: passRate={}%, avgScore={}",
report.getPassRate() * 100, report.getAverageScore());
return report;
}
private CaseResult evaluateSingle(
LLMSystem system,
EvaluationCase testCase,
EvaluationType type) {
// 调用被评估系统
String actualOutput = system.invoke(testCase.getInput());
return switch (type) {
case EXACT_MATCH -> evaluateExactMatch(testCase, actualOutput);
case CONTAINS_KEYWORDS -> evaluateKeywords(testCase, actualOutput);
case LLM_JUDGE -> evaluateWithLLM(testCase, actualOutput);
case SEMANTIC_SIMILARITY -> evaluateSemanticSimilarity(testCase, actualOutput);
};
}
/**
* LLM评估——让强模型来评估弱模型的输出质量
*/
private CaseResult evaluateWithLLM(EvaluationCase testCase, String actualOutput) {
String evalPrompt = String.format("""
请评估以下AI回答的质量,给出1-5分的评分并说明理由。
【用户问题】
%s
【参考答案】(可能为空)
%s
【AI实际回答】
%s
评分标准:
5分:完全正确,清晰准确,没有幻觉
4分:基本正确,有小瑕疵
3分:部分正确,有明显缺漏但无明显错误
2分:回答方向正确但内容有较多问题
1分:错误的、有害的或完全无关的回答
请严格按如下格式返回:
score: [分数]
reason: [原因,一到两句话]
""",
testCase.getInput(),
testCase.getExpectedOutput() != null ? testCase.getExpectedOutput() : "无参考答案",
actualOutput
);
LLMResponse evalResponse = evaluatorLLM.call(LLMRequest.of(evalPrompt));
return parseEvalResponse(testCase, actualOutput, evalResponse.getContent());
}
}Layer 5:运维与治理层
进入生产的AI系统,和传统服务一样需要完整的运维体系——但又有它的特殊性。
AI系统的特殊性体现在:
- 输出不确定性:同样的输入,不同时间可能有不同输出,用传统的"预期输出"来监控不适用
- 质量退化难发现:模型切版本、Prompt调整、RAG知识库更新都可能导致质量静默退化
- 成本可见性:Token消耗直接等于钱,需要精细化的成本追踪
/**
* AI系统生产监控
*
* 重点监控三类指标:
* 1. 可用性指标(调用成功率、延迟)
* 2. 质量指标(通过在线LLM评估或用户反馈信号)
* 3. 成本指标(Token消耗、每次调用成本)
*/
@Component
@Slf4j
public class AIProductionMonitor {
private final MeterRegistry registry;
private final AlertService alertService;
/**
* 记录一次AI调用的全量监控数据
*/
public void record(AICallTrace trace) {
// 可用性指标
registry.counter("ai.calls.total",
"feature", trace.getFeatureName(),
"status", trace.isSuccess() ? "success" : "error",
"error_type", trace.getErrorType()
).increment();
registry.timer("ai.latency",
"feature", trace.getFeatureName()
).record(trace.getLatencyMs(), TimeUnit.MILLISECONDS);
// 成本指标
registry.counter("ai.tokens.input",
"feature", trace.getFeatureName(),
"model", trace.getModel()
).increment(trace.getInputTokens());
registry.counter("ai.tokens.output",
"feature", trace.getFeatureName(),
"model", trace.getModel()
).increment(trace.getOutputTokens());
// 质量信号(如果有用户反馈)
if (trace.getUserFeedback() != null) {
registry.gauge("ai.quality.user_satisfaction",
List.of(Tag.of("feature", trace.getFeatureName())),
trace.getUserFeedback().getSatisfactionScore());
}
// 异常检测:延迟突然上升告警
checkLatencyAnomaly(trace);
// 成本预警:日消耗接近上限告警
checkCostThreshold(trace);
}
private void checkLatencyAnomaly(AICallTrace trace) {
// P95延迟阈值告警(实际生产中用滑动窗口计算基线)
if (trace.getLatencyMs() > 10000) {
alertService.sendAlert(Alert.builder()
.level(AlertLevel.WARNING)
.title("AI调用延迟超阈值")
.message(String.format("feature=%s latency=%dms 超过10秒",
trace.getFeatureName(), trace.getLatencyMs()))
.build());
}
}
}选型决策框架:如何根据场景选工具
最后,给出一个实用的决策框架:
根据团队情况选主框架:
if (团队语言 == Java && 项目类型 == 企业应用) {
主框架 = Spring AI;
} else if (需要复杂Agent) {
主框架 = LangChain4j;
}
根据数据规模选向量数据库:
if (文档数 < 100万 && 团队没有专职DBA) {
向量数据库 = pgvector (PostgreSQL扩展);
} else if (已有Elasticsearch) {
向量数据库 = Elasticsearch dense_vector;
} else if (文档数 > 1000万) {
向量数据库 = Milvus 或 Qdrant;
}
根据团队规模选评估工具:
if (团队 < 5人) {
评估 = 自建简单评估脚本 + 人工采样;
} else if (团队 < 20人) {
评估 = LangFuse 或 Phoenix;
} else {
评估 = 自建评估平台 + 以上开源工具;
}结语:技术栈是手段,工程能力是目标
这篇文章列了很多工具和框架,但我最想传递的一个观点是:
技术选型不是一次性决策,而是随团队成熟度演进的过程。
第一个月,你需要的可能只是一个靠谱的LLM客户端封装。三个月后,你需要加向量数据库和RAG。六个月后,你发现没有评估体系根本不知道系统变好变坏。一年后,才需要考虑完整的AI平台和治理体系。
很多团队犯的错误是第一天就想把所有层都搭建好,结果大量时间花在基础设施建设上,没有时间打磨真正的业务价值。
我的建议:优先交付业务价值,在遇到真实痛点时才引入新的基础设施层。
每一层的工具,等你真正需要它的时候再引入,你才知道该选什么。
