文末附两道高质量面试题 | 建议收藏
一、从36Kr热点说起:Codex正在颠覆OpenAI自己
今天的36Kr AI热榜上,有一条意味深长的新闻:「造ChatGPT的人,已经不用ChatGPT干活了」。据报道,OpenAI内部已经全面启用Codex作为主力编程工具,不到一年时间,ChatGPT在自家内部AI编程工具的份额已被Codex超越。
这则新闻看似只是在讲OpenAI的内部工具迭代,但细想之下,它揭示了一个更深层的趋势:AI编程已经从「辅助人类编程」进化到「AI系统自主完成编程任务」的阶段。Codex不仅仅是补全代码,它能够理解需求、拆解任务、编写测试、提交PR——这是一个完整的AI Agent工作流。
无独有偶,另一条热榜新闻同样引人注目:Claude Opus 4.8在编程评测中取得了87.1%的惊人成绩,但断网后直接暴跌至73.0%,其中63%的「解题」竟非独立推导。这说明什么?当前的顶级AI模型在编程任务上严重依赖外部信息检索能力。
🔍 关键洞察:这两条新闻共同指向一个核心能力——AI Agent的检索增强生成(RAG)能力。无论是Codex还是其他AI编程工具,它们的核心竞争力之一就是如何高效地「知道该查什么文档、该用什么API、该参考什么代码」。
对于我们Java工程师来说,这恰恰是切入AI工程侧的最佳切入点之一:LangChain——这个已经成为AI Agent开发事实标准的框架。
二、LangChain是什么:重新理解「链式」二字
很多Java工程师第一次接触LangChain时,会被它的名字误导——以为它只是一个「链接LLM调用」的工具。这低估了它的价值。
LangChain的核心设计哲学是:将AI应用开发中的各种能力「模块化」,再通过「组合」的方式构建复杂应用。「链」只是这种组合形式的一种,它代表的是一种有序的、可插拔的数据处理流水线。
2.1 LangChain的四大核心模块
从架构层面,LangChain可以分为四个主要部分:
📦 Models
LLM接口抽象层,支持OpenAI、Anthropic、HuggingFace等100+模型
🔗 Prompts
提示词模板管理,支持动态变量注入和复用
📚 Indexes
文档加载、分割、向量检索——RAG的核心支撑
🤖 Agents
推理引擎+工具调用,实现自主决策和执行
2.2 为什么Java工程师必须关注LangChain
可能有Java工程师会问:LangChain是Python框架,跟我有什么关系?
这个问题要分两层看:
第一层:概念迁移。LangChain代表的是AI应用架构的范式转移。无论你用Python的LangChain还是Java的实现,核心概念是相通的。理解LangChain的四大模块,你就能理解市面上90%的AI应用是如何构建的。
第二层:工程落地。在国内的企业级市场,Java是绝对主流。当AI能力需要与现有Java系统集成时,你需要理解LangChain的设计思路,才能做出好的技术方案。
💡 类比理解:把LangChain想象成Java里的Spring Framework。Spring不是银弹,但它定义了一套IoC/AOP的思维模式。LangChain同样,它定义了一套AI应用开发的思维模式。即使你不写Python,这套思维模式你必须掌握。
三、深入LangChain核心:从原理到代码
理解了LangChain是什么,接下来我们深入它的核心组件。由于这是面向有Java基础的工程师,我会用Java伪代码来展示核心概念,帮助你建立技术直觉。
3.1 模型层(Models):一切AI能力的起点
LangChain的Models层做了两件事:统一接口抽象和能力扩展。
以Java伪代码理解:
// Java伪代码:理解LangChain的Model抽象
public interface LLMModel {
// 核心调用:给定提示词,返回文本响应
String generate(String prompt);
// 流式调用:适用于需要实时展示生成过程的场景
Stream<String> generateStream(String prompt);
}
// OpenAI实现
public class OpenAIGPT implements LLMModel {
private final String apiKey;
private final String modelName; // "gpt-4", "gpt-3.5-turbo"
@Override
public String generate(String prompt) {
// 底层调用OpenAI API
return openaiClient.chat().create(prompt, modelName);
}
}
// 使用示例:切换模型就像换实现类一样简单
LLMModel model = new OpenAIGPT("sk-xxx");
String response = model.generate("解释一下什么是RAG");
// 切换到Claude
LLMModel model2 = new AnthropicClaude("sk-ant-xxx");
String response2 = model2.generate("解释一下什么是RAG");
这个抽象的价值在于:你的业务代码不依赖具体模型。当GPT-5发布时,你只需要换一行配置,而不是重构整个代码库。
3.2 提示词工程(Prompts):LLM的「编程语言」
如果说LLM是AI能力的引擎,那Prompt就是「驾驶手册」。LangChain的Prompts模块帮你管理这些手册。
// Java伪代码:理解LangChain的Prompt模板
public class PromptTemplate {
private final String template; // "你好,我叫{name},我喜欢{interest}"
private final List<String> variableNames; // ["name", "interest"]
// 渲染模板:把变量填进去
public String render(Map<String, Object> variables) {
String result = template;
for (Map.Entry<String, Object> entry : variables.entrySet()) {
result = result.replace("{" + entry.getKey() + "}",
String.valueOf(entry.getValue()));
}
return result;
}
}
// 实际使用
PromptTemplate template = new PromptTemplate(
"作为一个{role},请分析以下问题:{question}"
);
Map<String, Object> vars = new HashMap<>();
vars.put("role", "高级软件架构师");
vars.put("question", "微服务架构的优缺点");
String prompt = template.render(vars);
// 输出: "作为一个高级软件架构师,请分析以下问题:微服务架构的优缺点"
LangChain还支持更高级的Prompt组合——将多个子Prompt拼接成复杂的指令链。这在RAG场景中特别有用:检索到的文档内容作为Context,与用户问题一起组装成最终Prompt。
3.3 索引层(Indexes):RAG的工程基石
这是LangChain最重要的模块之一,也是当前AI应用的核心能力——让LLM「懂」你的私有数据。
RAG(Retrieval-Augmented Generation)的流程可以理解为:
1️⃣ 索引(Indexing):将文档切分成小块,嵌入成向量,存入向量数据库
2️⃣ 检索(Retrieval):用户提问时,将问题也嵌入成向量,从数据库中找回最相关的文档块
3️⃣ 生成(Generation):将检索到的文档块作为上下文,连同用户问题一起发给LLM生成回答
🔍 生活类比:想象你在一个大型图书馆(向量数据库)里找书。你不是记住每本书的内容,而是记住每本书的「核心主题向量」。当你想找「如何教育小孩」的书时,你的思维向量会指向家庭教育区域,然后你取出该区域的相关书籍阅读,再用自己的话总结回答。这,就是RAG。
Java伪代码展示核心流程:
// Java伪代码:RAG的核心流程
// ========== 第一阶段:索引(Indexing)==========
// 1. 文档加载
DocumentLoader loader = new PDFLoader("./内部知识库.pdf");
List<Document> documents = loader.load();
// 2. 文档分割:切成合适大小的块
TextSplitter splitter = new RecursiveCharacterTextSplitter(
1000, // 每个块1000字符
200 // 块之间重叠200字符(保持上下文连贯)
);
List<Document> chunks = splitter.split(documents);
// 3. 向量化:调用Embedding模型
EmbeddingModel embedder = new OpenAIEmbedding("sk-xxx");
List<Vector> vectors = embedder.embed(chunks);
// 4. 存入向量数据库(以Qdrant为例)
QdrantStore vectorStore = new QdrantStore(
"localhost", 6334, // Qdrant服务地址
"my_collection" // 集合名
);
vectorStore.add(vectors, chunks);
// ========== 第二阶段:检索(Retrieval)==========
// 5. 用户提问
String userQuestion = "公司年假政策是什么?";
// 6. 将问题向量化
Vector queryVector = embedder.embed(userQuestion);
// 7. 相似度检索:找回最相关的3个文档块
List<RetrievedDocument> relevantDocs =
vectorStore.similaritySearch(queryVector, topK=3);
// ========== 第三阶段:生成(Generation)==========
// 8. 组装Prompt:将检索结果作为上下文
PromptTemplate promptTemplate = new PromptTemplate(
"根据以下上下文回答问题。\n\n" +
"上下文:\n{context}\n\n" +
"问题:{question}\n" +
"回答:"
);
Map<String, Object> variables = new HashMap<>();
variables.put("context", relevantDocs.stream()
.map(d -> d.getContent())
.collect(Collectors.joining("\n---\n")));
variables.put("question", userQuestion);
String finalPrompt = promptTemplate.render(variables);
// 9. 调用LLM生成回答
LLMModel llm = new OpenAIGPT("sk-xxx");
String answer = llm.generate(finalPrompt);
System.out.println(answer);
3.4 Agent层:让AI「会思考」
如果说RAG是让AI「有知识」,那Agent就是让AI「会行动」。Agent是LangChain最强大、也最复杂的部分。
简单来说,Agent = LLM(推理引擎)+ Tools(工具集)+ 执行循环
// Java伪代码:理解Agent的执行循环
public class AIProgrammingAgent {
private LLMModel llm;
private List<Tool> tools; // 可用工具:搜索、代码执行、文件操作等
public String execute(String task) {
String currentTask = task;
int maxIterations = 10;
for (int i = 0; i < maxIterations; i++) {
// Step 1: LLM思考——我该做什么?
ThoughtProcess thought = llm.think(
"任务:{currentTask}\n" +
"可用工具:{tools}\n" +
"决定下一步行动:"
);
// Step 2: 解析LLM的决定
if (thought.isFinished()) {
return thought.getFinalAnswer();
}
// Step 3: 执行工具调用
String toolName = thought.getToolToUse();
Map<String, Object> toolArgs = thought.getToolArgs();
ToolResult result = executeTool(toolName, toolArgs);
// Step 4: 将工具执行结果反馈给LLM,继续推理
currentTask = task + "\n已执行:" + toolName +
"\n结果:" + result.getOutput();
}
return "任务未能在限制次数内完成";
}
}
// Agent的典型工作流示例
// 任务:"在Java项目中添加一个用户登录功能"
AIProgrammingAgent agent = new AIProgrammingAgent(
new OpenAIGPT("sk-xxx"),
Arrays.asList(
new FileSearchTool(), // 搜索现有代码结构
new FileWriteTool(), // 写入代码文件
new CodeExecuteTool(), // 运行测试
new GitTool() // Git操作
)
);
String result = agent.execute(
"在Java项目中添加一个用户登录功能,包括Controller、Service、DAO三层"
);
这就是OpenAI Codex背后的核心逻辑。只不过Codex整合了更强大的模型和更丰富的工具链。对Java工程师来说,理解Agent的执行循环,是设计AI自动化流程的关键。
四、工程实战:构建一个知识库问答系统
理论讲完了,我们来看一个真实的工程场景:用LangChain的思路,设计一个企业内部知识库问答系统。
4.1 整体架构设计
🏗️ 系统架构:
用户提问 → 向量化检索(Qdrant)→ 上下文组装 → LLM生成 → 返回答案
关键技术选型:
• 向量数据库:Qdrant(Rust实现,高性能、支持混合检索)
• Embedding模型:text-embedding-3-small(OpenAI)或中文BGE
• LLM:GPT-4 / Claude-3.5 / 国内通义/文心(按需切换)
• 文档处理:PDF、Word、Markdown等多格式支持
4.2 核心Java实现
// ==================== Part 1: 文档索引流程 ====================
@Service
public class DocumentIndexingService {
@Autowired
private QdrantClient qdrantClient;
@Autowired
private EmbeddingService embeddingService;
@Autowired
private DocumentParserFactory parserFactory;
/**
* 索引一份文档到向量数据库
*/
public void indexDocument(File file) throws Exception {
// 1. 根据文件类型选择解析器
DocumentParser parser = parserFactory.getParser(file.getType());
Document document = parser.parse(file);
// 2. 文本清洗和预处理
String cleanedText = cleanText(document.getContent());
// 3. 语义分块(不同于简单的按字数分割)
List<TextChunk> chunks = semanticChunk(cleanedText);
// 4. 批量向量化(注意:生产环境要批量处理,避免API限流)
List<float[]> embeddings = embeddingService.embedBatch(
chunks.stream()
.map(TextChunk::getContent)
.collect(Collectors.toList())
);
// 5. 构建向量Payload,存入Qdrant
List<PointStruct> points = new ArrayList<>();
for (int i = 0; i < chunks.size(); i++) {
Map<String, Object> payload = new HashMap<>();
payload.put("content", chunks.get(i).getContent());
payload.put("source", file.getName());
payload.put("chunk_id", chunks.get(i).getId());
payload.put("metadata", chunks.get(i).getMetadata());
points.add(PointStruct.builder()
.id(UUID.randomUUID().toString())
.vector(embeddings.get(i))
.payload(payload)
.build());
}
qdrantClient.upsert("knowledge_base", points);
}
/**
* 语义分块:按段落和语义边界分割,而非简单字数分割
*/
private List<TextChunk> semanticChunk(String text) {
// 实际实现会更复杂,这里简化展示
List<TextChunk> chunks = new ArrayList<>();
String[] paragraphs = text.split("\n\n");
StringBuilder currentChunk = new StringBuilder();
for (String para : paragraphs) {
if (currentChunk.length() + para.length() < 1000) {
currentChunk.append(para).append("\n\n");
} else {
chunks.add(new TextChunk(currentChunk.toString()));
currentChunk = new StringBuilder(para).append("\n\n");
}
}
if (currentChunk.length() > 0) {
chunks.add(new TextChunk(currentChunk.toString()));
}
return chunks;
}
}
// ==================== Part 2: 问答检索流程 ====================
@Service
public class QAService {
@Autowired
private QdrantClient qdrantClient;
@Autowired
private EmbeddingService embeddingService;
@Autowired
private LLMClient llmClient;
private static final int TOP_K = 5;
private static final double SIMILARITY_THRESHOLD = 0.75;
public AnswerResult answer(String question) {
// 1. 问题向量化
float[] questionEmbedding = embeddingService.embed(question);
// 2. 向量检索(支持混合检索:语义+关键词)
SearchParams params = SearchParams.builder()
.hnswParams(HnswParams.builder().ef(128).build())
.build();
List<ScoredPoint> searchResults = qdrantClient.search(
"knowledge_base",
questionEmbedding,
TOP_K,
params
);
// 3. 过滤低相似度结果
List<String> relevantContexts = searchResults.stream()
.filter(r -> r.getScore() >= SIMILARITY_THRESHOLD)
.map(r -> (String) r.getPayload().get("content"))
.collect(Collectors.toList());
if (relevantContexts.isEmpty()) {
return new AnswerResult(
"抱歉,我在知识库中没有找到与您问题相关的信息。",
Collections.emptyList()
);
}
// 4. 构建Prompt
String context = relevantContexts.stream()
.collect(Collectors.joining("\n---\n"));
String prompt = buildPrompt(context, question);
// 5. LLM生成回答
String answer = llmClient.generate(prompt);
// 6. 返回结果(包含引用来源,便于溯源)
List<Citation> citations = searchResults.stream()
.filter(r -> r.getScore() >= SIMILARITY_THRESHOLD)
.map(r -> new Citation(
(String) r.getPayload().get("source"),
r.getScore()
))
.collect(Collectors.toList());
return new AnswerResult(answer, citations);
}
private String buildPrompt(String context, String question) {
return String.format("""
你是一个专业的企业内部助手。请根据以下参考信息回答用户问题。
参考信息:
%s
用户问题:%s
要求:
1. 仅基于参考信息回答,不要编造信息
2. 如果参考信息不足以回答,请明确说明
3. 回答要专业、准确、易懂
回答:
""", context, question);
}
}
4.3 面试题点睛
上面的代码设计涉及几个常见的面试考点:
| 考点 | 考察点 | 难度 |
|---|---|---|
| 向量数据库索引策略 | HNSW vs IVF, ef参数调优 | ⭐⭐⭐ |
| 语义分块算法 | 如何保证语义完整性 | ⭐⭐⭐⭐ |
| 混合检索 | 向量检索+关键词检索如何融合 | ⭐⭐⭐⭐ |
| Prompt Engineering | 如何设计RAG场景的Prompt | ⭐⭐⭐ |
| 系统设计 | 如何支持多用户、高并发 | ⭐⭐⭐⭐⭐ |
五、面试实战:两道高频考点题
结合今天的热点和LangChain的核心知识,我出两道面试题:
📝 面试题一:RAG场景下的向量检索优化
假设你在为公司构建一个基于RAG的客服系统,当前遇到了检索质量问题:用户问「如何重置登录密码」,系统经常返回一些「密码安全性要求」之类的相关但不精准的文档,而真正「重置密码操作指南」反而排后面。
请分析可能的原因,并给出至少3种优化方案。
参考答案:
原因分析:
1. Embedding模型不匹配:通用Embedding模型(如OpenAI的ada)对中文长尾query的语义理解不够精准
2. 分块策略问题:文档被切成均匀的小块,可能破坏了「重置密码」这一完整操作流程的上下文连贯性
3. 检索策略单一:仅用向量相似度检索,缺乏关键词命中来「boost」精准匹配的文档
4. 元信息利用不足:文档的标题、标签等元信息没有用于二次排序或过滤
优化方案:
方案1:混合检索(Hybrid Search)
结合向量相似度检索和BM25关键词检索,用RRF(Reciprocal Rank Fusion)融合两路结果:
Score = 0.6 × vector_sim + 0.4 × keyword_match
方案2:语义分块优化
使用更智能的分块策略:按语义段落分块,保留完整的操作步骤;增加块之间的重叠(overlap),保证跨块上下文;为每个块补充前置/后置依赖信息。
方案3:Query改写与扩展
用LLM对用户query进行改写,生成多个同义query(如「找回密码」「忘记密码处理」「账户恢复」),对多个query分别检索后合并结果,提升召回率。
方案4:重新排序(Re-ranking)
用Cohere等支持Re-ranking的模型,对初筛结果进行相关性重排,将「重置密码操作指南」这类精准匹配文档提升到前面。
📝 面试题二:AI Agent的记忆机制设计
你在设计一个AI客服Agent,它需要记住与用户的对话历史,以便在多轮对话中保持上下文一致性。当前你考虑三种方案:
1. 每次请求把完整历史对话传给LLM
2. 只传最近N轮对话
3. 使用「记忆模块」动态管理对话摘要
请分析三种方案的优劣,并结合LangChain/Mem0等框架的设计理念,设计一个适合企业级客服场景的记忆机制。
参考答案:
三种方案对比:
方案1(完整历史):优点是上下文完整,缺点是成本高(token费用)、延迟大,且超过LLM上下文窗口后无法处理。
方案2(固定窗口):实现简单,但可能丢失重要历史信息,且对对话长度有硬性限制。
方案3(动态记忆):平衡了信息完整性和成本,是工程主流选择。
企业级记忆机制设计(参考Mem0思想):
第一层:短期记忆(Short-term)
当前会话的滑动窗口,用Semantic Chunking动态压缩,保留最近3-5轮的核心语义+关键实体(用户信息、任务状态、待办事项)。
第二层:长期记忆(Long-term)
用户画像和偏好(如沟通风格、行业背景、历史问题类型),存储在向量数据库中,每次对话自动检索相关历史上下文。
第三层:情节记忆(Episodic)
每个会话周期的关键摘要,独立存储。支持「回忆」:当用户再次提起历史话题时,能快速检索相关情节。
Java实现伪代码:
public class ConversationMemory {
// 短期记忆:当前会话滑动窗口
private List<Message> recentMessages; // 保留最近5轮
// 长期记忆:用户特征向量
private Map<String, float[]> userMemory; // userId -> 记忆向量
// 情节记忆:会话摘要
private List<EpisodeSummary> episodes;
public String buildContext(String userId, String currentQuery) {
// 1. 获取短期记忆(最近对话)
String shortTerm = buildShortTermContext();
// 2. 检索长期记忆(用户画像相关)
String longTerm = retrieveLongTermMemory(userId, currentQuery);
// 3. 检索情节记忆(历史相关)
String episodic = retrieveEpisodicMemory(userId, currentQuery);
return shortTerm + "\n" + longTerm
夜雨聆风