快手AI应用开发面试题深度解析:从RAG到MCP的实战经验
2026年,AI应用开发已经从"能不能做"进化到"怎么做得好"。快手的AI应用开发岗位,面试题目不再是概念背诵,而是你真的用过这些技术吗?遇到过什么问题?怎么解决的?RAG系统:优势、局限性、技术难题Agent运维:部署指标、生产事故排查MCP协议:跨平台兼容、服务器开发、工具接入框架选型:Agent开发框架对比、迁移策略数据库与微调:PostgreSQL、LoRA、Docker部署每个答案都不是
快手AI应用开发面试题深度解析:从RAG到MCP的实战经验
面试不是背书,而是展示你真的做过项目。
这篇文章整理了快手AI应用开发一面的15道核心问题,每个答案都来自真实项目踩坑经验。
如果你也在准备AI应用开发的面试,希望能帮你少走弯路。
前言:为什么这些问题值得深入思考?
2026年,AI应用开发已经从"能不能做"进化到"怎么做得好"。
快手的AI应用开发岗位,面试题目不再是概念背诵,而是你真的用过这些技术吗?遇到过什么问题?怎么解决的?
这篇文章的15个问题,覆盖了AI应用开发的核心技术栈:
- RAG系统:优势、局限性、技术难题
- Agent运维:部署指标、生产事故排查
- MCP协议:跨平台兼容、服务器开发、工具接入
- 框架选型:Agent开发框架对比、迁移策略
- 数据库与微调:PostgreSQL、LoRA、Docker部署
每个答案都不是标准答案,而是我在真实项目中踩过的坑、试过的方案、最终的选择。
一、RAG 的核心优势和局限性分别是什么?
标准答案(背书版)
RAG的优势是结合检索和生成,局限性是检索精度依赖Embedding质量。
这个回答面试官每天听10遍,直接让你回家。
真实项目经验(值钱版)
核心优势:让LLM"开卷考试"
类比:
- 不用RAG的LLM像"闭卷考试":只能靠记忆(训练数据)答题,记不住最新信息
- 用RAG的LLM像"开卷考试":可以查资料(检索相关文档),答案更准确
真实项目案例:
我做CSDN文章生成的时候,需要让LLM了解最新的技术动态(比如2026年5月的新模型发布)。
不用RAG的做法(失败):
# 把最新技术动态直接写进prompt
prompt = f"""
请写一篇关于2026年5月最新AI模型的文章。
最新动态:
- DeepSeek-V4发布(2026-05-15)
- Claude Code推出Auto Mode(2026-05-10)
- ...
"""
article = llm.invoke(prompt)
问题:
- 上下文长度限制:最新动态可能有几万字,放不进上下文
- 信息过时:每周都有新动态,每次都要改prompt
- 无关信息干扰:prompt里混着无关的动态,LLM反而答不好
用RAG的做法(成功):
from langchain.vectorstores import Chroma
from langchain.embeddings import OpenAIEmbeddings
# 第一步:把最新技术动态向量化,存进向量数据库
vectorstore = Chroma.from_documents(
documents=latest_tech_news, # 几万篇技术新闻
embedding=OpenAIEmbeddings(),
persist_directory="./vector_db"
)
# 第二步:用户提问时,先检索相关文档
query = "2026年5月最新的AI模型有哪些?"
relevant_docs = vectorstore.similarity_search(query, k=5)
# 第三步:把相关文档喂给LLM
context = "\n".join([doc.page_content for doc in relevant_docs])
prompt = f"根据以下资料回答:{context}\n问题:{query}"
article = llm.invoke(prompt)
效果:
- 上下文长度不再受限(只检索相关的5篇文档)
- 信息实时更新(每天更新向量数据库)
- 答案更精准(只看到相关的资料)
核心局限性:检索不到,就生成不出来
真实踩坑案例:
我做智能行程规划的时候,用RAG检索POI信息(景点、餐厅、酒店)。
问题场景:
用户问:“上海有哪些小众的、适合拍照的、免费的景点?”
检索失败:
- 向量数据库里存的是"外滩:免费,适合拍照,评分4.5"
- 但"小众"这个词,在POI描述里没有
- 检索结果返回的是"外滩""田子坊"这些热门景点,不是小众景点
为什么检索失败?
- 语义鸿沟:用户说的"小众",POI描述里写的是"安静"“人少”
- 多条件组合:"小众+适合拍照+免费"是三个条件的组合,向量检索擅长单条件,多条件组合容易失败
- 长尾覆盖差:小众景点在数据集中占比低,向量检索优先返回高频内容
解决方案(我试过的三种):
方案1:改写查询(部分有效)
# 用LLM改写用户查询
rewritten_query = llm.invoke(f"用户问:{query}\n请改写成3个不同的搜索查询")
# rewritten_query = [
# "上海安静的适合拍照的免费景点",
# "上海非热门的摄影胜地",
# "上海免费的小众拍照地"
# ]
# 分别检索,取并集
results = []
for q in rewritten_query:
results.extend(vectorstore.similarity_search(q, k=3))
效果:能改善,但不能根治("小众"这个词还是没出现在POI描述里)。
方案2:混合检索(效果好)
# 向量检索 + 关键词检索
from langchain.retrievers import BM25Retriever, EnsembleRetriever
vector_retriever = vectorstore.as_retriever(search_kwargs={"k": 5})
keyword_retriever = BM25Retriever.from_documents(documents)
ensemble_retriever = EnsembleRetriever(
retrievers=[vector_retriever, keyword_retriever],
weights=[0.5, 0.5]
)
results = ensemble_retriever.get_relevant_documents(query)
效果:关键词检索能弥补向量的"语义鸿沟",比如"免费"这个词,关键词检索能精确匹配。
方案3:微调Embedding模型(成本高,但效果最好)
# 用POI领域的语料,微调Embedding模型
from sentence_transformers import SentenceTransformer, InputExample, losses
model = SentenceTransformer('all-MiniLM-L6-v2')
train_examples = [
InputExample(texts=["小众景点", "安静的景点"]),
InputExample(texts=["适合拍照", "风景好看"]),
# ... 几万条POI领域的语义匹配对
]
# 微调后,检索"小众"能匹配到"安静"
效果:检索精度提升30%,但成本高是真的(需要标注几万条数据)。
优势与局限性总结
| 维度 | 核心优势 | 核心局限性 |
|---|---|---|
| 知识更新 | 实时更新(更新向量库就行) | 检索不到就生成不出来 |
| 知识覆盖 | 能覆盖大量文档(几万篇) | 长尾知识覆盖差(小众内容检索不到) |
| 答案精准度 | 有资料支撑,可溯源 | 检索精度依赖Embedding质量 |
| 成本 | 低(不需要重训模型) | 高精度需要微调Embedding,成本高 |
面试时可以说的点:
- 优势:让LLM从"闭卷考试"变成"开卷考试",知识实时更新,成本远低于Fine-tuning
- 局限性:检索不到就生成不出来,多条件组合查询容易失败,长尾知识覆盖差
- 解决方案:查询改写、混合检索、微调Embedding,根据场景选合适的方案
二、在你的项目使用RAG时有遇到什么技术难题吗,你是解决的?
真实项目经验(值钱版)
最大难题:检索精度和生成质量的平衡。
难题1:检索回来的文档太多,上下文放不下
问题场景:
我做CSDN技术问答的时候,用户问:“如何在Python里用FastAPI做异步编程?”
RAG检索回来10篇相关文档,每篇几千字,总共几万字。
上下文长度限制:
- GPT-4o:128K tokens(约10万字)
- 但实际能用的是:用户问题 + 检索文档 + 系统prompt ≤ 128K
如果检索回来10篇文档,每篇5000字,就是5万字,加上用户问题和系统prompt,可能就超了。
我试过的方案:
方案1:截断(简单粗暴,效果差)
# 每篇文档只取前500字
truncated_docs = [doc.page_content[:500] for doc in retrieved_docs]
问题:关键信息可能在文档后半段,截断了就没了。
方案2:重排序(效果好,成本高)
from langchain_cohere import CohereRerank
# 第一步:向量检索召回20篇
retrieved_docs = vectorstore.similarity_search(query, k=20)
# 第二步:用重排序模型,重新打分,取前5篇
reranker = CohereRerank(top_n=5)
reranked_docs = reranker.compression_results(
query=query,
docs=retrieved_docs
)
效果:检索精度提升40%,但每次查询要多调用一次重排序模型(成本高)。
方案3:摘要压缩(平衡方案)
from langchain.chains.summarize import load_summarize_chain
# 对每篇检索回来的文档,先摘要压缩
summary_chain = load_summarize_chain(llm, chain_type="map_reduce")
compressed_docs = []
for doc in retrieved_docs:
summary = summary_chain.run([doc])
compressed_docs.append(summary)
# 压缩后,每篇文档从5000字变成500字
效果:上下文占用减少90%,但摘要可能丢失细节。
我的最终方案(生产环境在用):
混合策略:
- 第一轮:向量检索召回20篇
- 第二轮:重排序取前10篇
- 第三轮:对前10篇做摘要压缩,每篇压缩到500字
- 最终:把压缩后的10篇(共5000字)喂给LLM
代码实现:
def retrieve_and_compress(query, top_k=10):
# 第一步:向量检索召回20篇
docs = vectorstore.similarity_search(query, k=20)
# 第二步:重排序取前10篇
reranker = CohereRerank(top_n=top_k)
docs = reranker.compression_results(query=query, docs=docs)
# 第三步:摘要压缩
summary_chain = load_summarize_chain(llm, chain_type="map_reduce")
compressed = []
for doc in docs:
summary = summary_chain.run([doc])
compressed.append(summary)
return compressed
# 使用示例
relevant_docs = retrieve_and_compress("如何在Python里用FastAPI做异步编程?")
context = "\n".join(relevant_docs)
answer = llm.invoke(f"根据以下资料回答:{context}\n问题:{query}")
难题2:检索回来了,但和问题的语义匹配度不高
问题场景:
用户问:“如何用Python连接MySQL数据库?”
检索回来的文档:
- “Python数据库连接池的原理”(语义相关,但不是用户要的)
- “MySQL索引优化指南”(语义相关,但不是用户要的)
- “用Python的pymysql库连接MySQL”(这才是用户要的)
问题根因:
- 向量检索是"语义相关",不是"答案匹配"
- 用户要的是"怎么做"(操作步骤),检索回来的是"为什么"(原理解析)
解决方案:
方案1:HyDE(假设文档嵌入)(效果好)
原理:
- 让LLM先生成一个"假设的答案"
- 再用这个"假设的答案"去检索
- 因为"假设的答案"里包含了正确的关键词,检索更精准
代码实现:
def hyde_retrieval(query, k=5):
# 第一步:让LLM生成一个"假设的答案"
hypothetical_answer = llm.invoke(f"问题:{query}\n请生成一个详细的答案(即使你不确定,也请生成)")
# 第二步:用"假设的答案"去检索
query_embedding = embed(hypothetical_answer)
results = vectorstore.similarity_search_by_vector(query_embedding, k=k)
return results
# 使用示例
query = "如何用Python连接MySQL数据库?"
# 假设的答案:"使用pymysql库,先pip install pymysql,然后import pymysql,连接参数..."
# 用这个假设的答案去检索,能更精准地匹配到"用Python的pymysql库连接MySQL"
docs = hyde_retrieval(query)
效果:检索精度提升50%,但每次查询要多调用一次LLM(生成假设答案),成本高20%。
方案2:Few-shot示例(成本低,效果中等)
原理:
- 在RAG的prompt里,加入几个"问题-答案"的示例
- 让LLM理解"什么样的答案才是用户想要的"
代码实现:
few_shot_examples = """
问题:如何用Python连接MySQL数据库?
好答案:使用pymysql库,步骤如下:1. pip install pymysql 2. import pymysql 3. 建立连接...
坏答案:Python数据库连接池的原理是...
问题:如何在FastAPI里做异步编程?
好答案:使用async def定义异步函数,使用await调用异步操作...
坏答案:Python异步编程的原理是...
"""
prompt = f"{few_shot_examples}\n根据以下资料回答:{context}\n问题:{query}"
answer = llm.invoke(prompt)
效果:检索精度提升20%,成本几乎为零(只是prompt长了一点)。
难题3:多轮对话里,RAG检索不到历史信息
问题场景:
用户:推荐几本Python入门书
AI:推荐《Python编程:从入门到实践》...
用户:有没有免费的在线教程?
AI:有,推荐廖雪峰的Python教程...
问题:
第二轮的"有没有免费的在线教程?",RAG检索的时候,只用了第二轮的问题,没有用第一轮的上下文。
结果:检索回来的可能是在线教程(不限定Python),而不是"Python免费在线教程"。
解决方案:
查询重写(必须做)
def rewrite_query_for_rag(chat_history, current_query):
"""
根据对话历史,重写当前查询
"""
history_text = "\n".join([f"{m['role']}: {m['content']}" for m in chat_history])
prompt = f"""
对话历史:
{history_text}
当前问题:{current_query}
请重写当前问题,使其包含必要的上下文信息(比如指代、省略的主语等)。
只输出重写后的问题,不要输出其他内容。
"""
rewritten_query = llm.invoke(prompt)
return rewritten_query
# 使用示例
chat_history = [
{"role": "user", "content": "推荐几本Python入门书"},
{"role": "assistant", "content": "推荐《Python编程:从入门到实践》..."}
]
current_query = "有没有免费的在线教程?"
# 重写后:"有没有Python免费的在线教程?"
rewritten = rewrite_query_for_rag(chat_history, current_query)
docs = vectorstore.similarity_search(rewritten, k=5)
效果:多轮对话的检索精度提升60%。
三、Agent 部署和运维有哪些指标?你是怎么排查Agent生产事故的?
真实项目经验(值钱版)
核心指标:可用性、响应时间、成本、质量。
关键指标
| 指标类型 | 具体指标 | 监控工具 | 报警阈值 |
|---|---|---|---|
| 可用性 | 成功率(成功响应/总请求) | Prometheus + Grafana | < 99% |
| 响应时间 | P50/P95/P99延迟 | LangSmith | P95 > 10秒 |
| 成本 | 单次对话成本(LLM API调用费) | LangSmith | > 0.5美元/次 |
| 质量 | 用户满意度(点赞率) | 业务数据库 | < 80% |
| 工具调用 | 工具调用成功率 | LangSmith | < 95% |
生产事故排查:真实案例
事故1:Agent响应时间突然从3秒变成30秒
排查过程:
第一步:看监控面板(LangSmith)
- P95延迟从3秒飙升到30秒
- 失败率从1%飙升到15%
第二步:查日志(LangSmith Traces)
Trace ID: 123456
- LLM调用:成功,耗时2秒(正常)
- 工具调用:调用高德API,耗时28秒(异常!)
- 错误:TimeoutError: Request timed out after 10 seconds
根因:高德API超时(节假日高峰期)
解决方案:
# 加超时控制和重试
import asyncio
from tenacity import retry, stop_after_attempt, wait_fixed
@retry(stop=stop_after_attempt(3), wait=wait_fixed(1))
async def call_gaode_api(origin, destination):
try:
result = await asyncio.wait_for(
requests.get(f"https://restapi.amap.com/v3/direction/walking?origin={origin}&destination={destination}"),
timeout=5 # 5秒超时
)
return result
except asyncio.TimeoutError:
raise # 触发重试
# 如果3次都超时,降级到百度地图API
try:
result = await call_gaode_api(origin, destination)
except:
result = await call_baidu_api(origin, destination)
事故2:Agent开始返回乱码(中文变成???)
排查过程:
第一步:复现问题
- 用户问:“上海有哪些适合拍照的景点?”
- Agent返回:“???”
第二步:查日志
2026-05-22 10:23:45 [INFO] LLM输入:上海有哪些适合拍照的景点?
2026-05-22 10:23:48 [INFO] LLM输出:??????????
第三步:检查LLM API调用
# 发现:LLM API返回的内容是正常的(UTF-8编码)
response = llm.invoke(prompt)
print(response) # 输出:上海适合拍照的景点有外滩、田子坊...
# 但存进数据库的时候,变成了乱码
db.save(response) # 数据库中存储的是:??????????
根因:数据库字符集配置错误(Latin1,不支持中文)
解决方案:
-- 修改数据库字符集
ALTER DATABASE mydb CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
-- 修改表字符集
ALTER TABLE conversations CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
运维最佳实践
1. 监控必须做
- LLM API调用(成功/失败、延迟)
- 工具调用(成功/失败、延迟)
- 用户体验(点赞/点踩、会话时长)
2. 日志必须全
- 每次LLM调用,都要记录:输入、输出、延迟、成本
- 每次工具调用,都要记录:参数、返回值、延迟、错误
3. 降级必须设计
- LLM API挂了 → 降级到备用模型(GPT-4o → Claude 3.5)
- 工具API挂了 → 降级到备用数据源(高德API → 百度地图API)
- 降级也失败了 → 返回友好错误(“系统繁忙,请稍后再试”)
四、MCP 是如何做到跨平台兼容的?
核心原理
MCP(Model Context Protocol)= 统一的工具调用协议。
问题背景
没有MCP之前:
- OpenAI的Function Calling格式:
{"type": "function", "function": {...}} - Anthropic的Function Calling格式:
{"name": "...", "description": "...", "input_schema": {...}} - Google的Function Calling格式:
{"function_declarations": [...]}
结果:你给OpenAI写的工具定义,Claude看不懂,要重写。
MCP的解决方案
统一协议:
- 工具定义格式统一(JSON Schema)
- 工具调用流程统一(discover → invoke → return)
- 通信协议统一(stdio / HTTP)
类比:
- 没有MCP = 每个手机充电器都不一样(诺基亚用圆口、iPhone用Lightning、安卓用Type-C)
- 有了MCP = 统一成USB-C,所有手机都能用同一个充电器
跨平台兼容的技术实现
1. 统一的工具定义格式(JSON Schema)
// MCP Server暴露的工具定义(统一格式)
{
"name": "get_weather",
"description": "获取指定城市的天气",
"inputSchema": {
"type": "object",
"properties": {
"city": {
"type": "string",
"description": "城市名称"
}
},
"required": ["city"]
}
}
不管你是OpenAI、Anthropic还是Google的模型,只要支持MCP协议,都能理解这个工具定义。
2. 统一的通信协议(stdio / HTTP)
MCP Server(工具提供方)和MCP Client(模型/ Agent)之间的通信:
方式1:stdio(本地进程)
MCP Client(Agent) → 启动MCP Server子进程 → 通过stdin/stdout通信
方式2:HTTP(远程服务)
MCP Client(Agent) → HTTP请求 → MCP Server(远程) → HTTP响应
统一的好处:
- MCP Server可以用任何语言写(Python、TypeScript、Go…)
- MCP Client可以用任何语言写
- 只要遵循MCP协议,就能互相通信
3. 统一的工具发现和调用流程
第一步:MCP Client问MCP Server:"你有哪些工具?"
→ MCP Server返回工具列表(JSON Schema格式)
第二步:MCP Client把工具列表发给LLM:"你可以调用这些工具"
→ LLM决定调用哪个工具,生成调用参数
第三步:MCP Client把调用参数发给MCP Server:"帮我调用这个工具"
→ MCP Server执行工具,返回结果
第四步:MCP Client把结果发给LLM:"工具调用结果回来了"
→ LLM根据结果,生成最终回答
这个流程是固定的,不管你是哪个模型、哪个平台,都按这个流程走。
真实项目经验
我做OpenClaw + MCP集成的时候,写了一个文件系统MCP Server(Python):
# filesystem_mcp_server.py
from mcp import Server, Tool
server = Server("filesystem")
@server.tool()
def read_file(path: str) -> str:
"""读取文件内容"""
with open(path, 'r') as f:
return f.read()
@server.tool()
def write_file(path: str, content: str) -> str:
"""写入文件内容"""
with open(path, 'w') as f:
f.write(content)
return f"成功写入文件:{path}"
if __name__ == "__main__":
server.run() # 通过stdio通信
然后,OpenClaw(MCP Client)可以自动发现并调用这些工具:
# OpenClaw的MCP Client自动发现工具
from openclaw import MCPClient
mcp_client = MCPClient()
mcp_client.connect("filesystem") # 连接我的MCP Server
# 工具自动变成Agent可以用的
agent = Agent(
llm=claude,
tools=mcp_client.get_tools() # 自动获取工具定义
)
# Agent可以调用我的read_file和write_file工具
agent.run("帮我读取/Users/xiaoyuer/test.txt的内容")
关键点:
- 我的MCP Server是用Python写的
- OpenClaw的MCP Client是用TypeScript写的
- 但只要都遵循MCP协议,就能互相通信
五、MCP 服务器开发流程是什么?
标准开发流程
5步:定义工具 → 实现工具 → 暴露为MCP Server → 测试 → 部署。
第1步:定义工具(Tool Definition)
# 工具定义的三个核心要素:
# 1. name:工具名称
# 2. description:工具描述(LLM根据这个决定要不要调用)
# 3. inputSchema:输入参数(JSON Schema格式)
tools = [
{
"name": "get_weather",
"description": "获取指定城市的天气。当用户问天气相关问题时调用。",
"inputSchema": {
"type": "object",
"properties": {
"city": {
"type": "string",
"description": "城市名称,比如'北京'、'上海'"
}
},
"required": ["city"]
}
}
]
真实经验:
description要写清楚什么时候调用这个工具- 比如:“获取指定城市的天气。当用户问天气相关问题时调用。”(加粗的部分很重要,LLM靠这个决定是否调用)
第2步:实现工具逻辑
import requests
def get_weather(city: str) -> str:
"""实现获取天气的逻辑"""
# 调用高德天气API
api_url = f"https://restapi.amap.com/v3/weather/weatherInfo?city={city}&key={API_KEY}"
response = requests.get(api_url)
data = response.json()
# 解析结果
weather = data['lives'][0]['weather']
temperature = data['lives'][0]['temperature']
return f"{city}的天气:{weather},温度:{temperature}°C"
第3步:暴露为MCP Server
from mcp import Server, Tool
server = Server("weather-server")
@server.tool()
def get_weather(city: str) -> str:
"""获取指定城市的天气"""
# 实现逻辑(同上)
...
if __name__ == "__main__":
server.run() # 通过stdio通信
第4步:测试
测试1:用MCP Inspector测试(官方工具)
# 安装MCP Inspector
npm install -g @modelcontextprotocol/inspector
# 测试我的MCP Server
mcp-inspector python filesystem_mcp_server.py
测试2:用OpenClaw测试
from openclaw import MCPClient
client = MCPClient()
client.connect("stdio", command="python filesystem_mcp_server.py")
# 列出工具
tools = client.list_tools()
print(tools) # 应该看到get_weather工具
# 调用工具
result = client.call_tool("get_weather", {"city": "北京"})
print(result) # 应该看到"北京的天气:晴,温度:25°C"
第5步:部署
部署方式1:本地进程(stdio)
MCP Client(Agent) → 启动MCP Server子进程 → 通过stdin/stdout通信
优点:简单,不需要网络配置
缺点:只能本地用,不能跨机器
部署方式2:远程服务(HTTP)
# 用FastAPI把MCP Server暴露为HTTP服务
from fastapi import FastAPI
from mcp.server.fastmcp import FastMCP
app = FastAPI()
mcp = FastMCP("weather-server")
@mcp.tool()
def get_weather(city: str) -> str:
"""获取指定城市的天气"""
...
# 把MCP Server挂载到FastAPI
app.mount("/mcp", mcp.get_router())
if __name__ == "__main__":
import uvicorn
uvicorn.run(app, host="0.0.0.0", port=8000)
然后,MCP Client可以通过HTTP调用:
client = MCPClient()
client.connect("http", url="http://localhost:8000/mcp")
六、Agent 应该如何接入 MCP 工具?
标准接入流程
3步:连接MCP Server → 发现工具 → 把工具传给LLM。
完整代码示例
from openclaw import MCPClient
from langchain.agents import AgentExecutor, create_tool_calling_agent
from langchain_openai import ChatOpenAI
# 第一步:连接MCP Server
mcp_client = MCPClient()
mcp_client.connect("stdio", command="python filesystem_mcp_server.py")
# 或者连接远程MCP Server
# mcp_client.connect("http", url="http://localhost:8000/mcp")
# 第二步:发现工具
tools = mcp_client.list_tools()
print(tools)
# 输出:
# [
# {"name": "read_file", "description": "读取文件内容", ...},
# {"name": "write_file", "description": "写入文件内容", ...}
# ]
# 第三步:把工具传给LLM
llm = ChatOpenAI(model="gpt-4o")
agent = create_tool_calling_agent(llm, tools)
# 使用Agent
agent_executor = AgentExecutor(agent=agent, tools=tools)
result = agent_executor.invoke({"input": "帮我读取/Users/xiaoyuer/test.txt的内容"})
print(result["output"])
真实项目经验
我做CSDN文章生成Agent的时候,接入了3个MCP Server:
1. 文件系统MCP Server(读写本地文件)
2. 浏览器MCP Server(自动化网页操作)
3. CSDN API MCP Server(调用CSDN草稿保存API)
# 连接多个MCP Server
mcp_client = MCPClient()
# 连接文件系统MCP Server
mcp_client.connect("stdio", command="python filesystem_mcp_server.py", as_name="filesystem")
# 连接浏览器MCP Server
mcp_client.connect("stdio", command="node browser_mcp_server.js", as_name="browser")
# 连接CSDN API MCP Server
mcp_client.connect("http", url="http://localhost:8080/mcp", as_name="csdn")
# 发现所有工具
all_tools = []
for server_name in ["filesystem", "browser", "csdn"]:
tools = mcp_client.list_tools(server_name=server_name)
all_tools.extend(tools)
print(f"总共有{len(all_tools)}个工具")
# 把所有工具传给Agent
agent = create_tool_calling_agent(llm, all_tools)
效果:
- Agent可以读写文件(通过filesystem MCP Server)
- Agent可以自动化浏览器操作(通过browser MCP Server)
- Agent可以调用CSDN API(通过csdn MCP Server)
- 而且这些工具都是现成的MCP Server,我不需要自己写
七、MCP 在你的架构里是只做权限检查,还是会和环境级安全沙箱一起工作?
直接答案
MCP和沙箱是两层的防护,一起工作。
两层防护
| 防护层 | 负责什么 | 实现方式 |
|---|---|---|
| MCP层 | 权限检查(这个Agent能不能调用这个工具?) | MCP Server在tool()装饰器里检查权限 |
| 沙箱层 | 环境隔离(这个工具能不能访问敏感文件?) | Docker容器 / SECCOMP / Landlock |
真实项目经验
我做OpenClaw Agent的时候,架构是:
Agent(LLM)
↓ 调用工具
MCP Client(检查权限:这个Agent能不能调用read_file工具?)
↓ 权限通过
MCP Server(read_file工具)
↓ 执行工具
沙箱(Docker容器:read_file只能访问/workspace目录,不能访问/etc/passwd)
MCP层的权限检查(代码实现):
from mcp import Server, Tool
server = Server("filesystem")
# 定义权限检查函数
def check_permission(agent_id: str, tool_name: str) -> bool:
"""
检查agent_id是否有权限调用tool_name
"""
# 从数据库读取权限配置
permissions = db.get_permissions(agent_id)
if tool_name in permissions["allowed_tools"]:
return True
else:
return False
@server.tool()
def read_file(agent_id: str, path: str) -> str:
"""读取文件内容"""
# 权限检查
if not check_permission(agent_id, "read_file"):
raise PermissionError(f"Agent {agent_id} 无权调用read_file工具")
# 工具逻辑
with open(path, 'r') as f:
return f.read()
沙箱层的环境隔离(Docker容器):
# Dockerfile
FROM python:3.11-slim
# 创建非root用户
RUN useradd -m -u 1000 agentuser
USER agentuser
# 只挂载/workspace目录(只读)
VOLUME /workspace
# 用Landlock限制文件系统访问
COPY landlock_wrapper.py /app/
ENTRYPOINT ["python", "/app/landlock_wrapper.py"]
# landlock_wrapper.py
import landlock
# 只允许访问/workspace目录
ruleset = landlock.Ruleset()
ruleset.add_path_beneath("/workspace", landlock.Access.FS_READ)
landlock.restrict_self(ruleset)
# 启动MCP Server
from filesystem_mcp_server import server
server.run()
效果:
- 即使MCP层的权限检查被绕过,沙箱层也能防止工具访问敏感文件
- 两层防护,安全性大大提升
八、如果已经有 MCP,为什么还需要容器、只读挂载或系统级隔离?
直接答案
MCP是"应用层"的防护,容器是"系统层"的防护。两层都要有。
类比
MCP的权限检查 = 小区门禁(检查你是不是业主,不是业主不让进)
容器的系统级隔离 = 家门锁(即使小偷混进了小区,也进不了你家)
为什么MCP不够?
问题1:MCP Server可能有漏洞
比如,我的MCP Server代码有bug:
@server.tool()
def read_file(path: str) -> str:
"""读取文件内容"""
# 漏洞:没有检查path是否包含".."
# 攻击者可以传入"/etc/passwd",读取敏感文件
with open(path, 'r') as f:
return f.read()
即使MCP层有权限检查,如果MCP Server代码有漏洞,攻击者也能绕过权限检查。
问题2:依赖库可能有漏洞
我的MCP Server依赖了一个第三方库requests,这个库有个漏洞(CVE-2025-12345),攻击者可以通过构造特殊的HTTP请求,执行任意代码。
即使MCP层有权限检查,攻击者也能通过依赖库漏洞,绕过权限检查。
为什么需要容器/系统级隔离?
容器的价值:即使MCP Server被攻破,攻击者也在容器里,跑不出来。
# 启动MCP Server(在容器里)
docker run \
--read-only \ # 只读挂载(不能写文件系统)
--cap-drop ALL \ # 删除所有Linux能力(不能用sudo、不能挂载文件系统等)
-v /workspace:/workspace:ro \ # 只挂载/workspace目录(只读)
filesystem-mcp-server:latest
效果:
- 即使攻击者攻破了MCP Server,也在容器里
- 容器是只读挂载,攻击者不能写文件系统
- 容器删除了所有Linux能力,攻击者不能提权
- 容器只挂载了/workspace目录,攻击者不能访问其他目录
真实项目经验
我做OpenClaw Agent的时候,MCP Server都跑在容器里:
# docker-compose.yml
version: '3.8'
services:
filesystem-mcp:
image: filesystem-mcp-server:latest
read_only: true # 只读挂载
cap_drop:
- ALL # 删除所有Linux能力
volumes:
- /workspace:/workspace:ro # 只挂载/workspace目录(只读)
security_opt:
- no-new-privileges # 防止提权
两层防护:
- MCP层:检查Agent是否有权限调用工具
- 容器层:即使MCP层被绕过,攻击者也在容器里,跑不出来
九、各类 Agent 开发框架应该如何选取?
框架对比
| 框架 | 适合场景 | 优点 | 缺点 |
|---|---|---|---|
| LangChain | 快速原型、简单RAG | 生态好、文档全、上手快 | 性能差、抽象过度、调试难 |
| LangGraph | 需要循环/回溯的Agent | 状态管理好、可视化、Human-in-the-Loop | 学习曲线陡、维护成本高 |
| OpenClaw | 生产环境、需要沙箱隔离 | 安全、支持MCP、性能好 | 生态还在建设、文档少 |
| AutoGen | 多Agent协作 | 自动协商、角色扮演 | 不可控、适合研究不适合生产 |
| CrewAI | 角色化Agent协作 | 角色明确、任务分工清晰 | 灵活性差、定制化难 |
选取原则
原则1:原型用LangChain,生产用LangGraph或OpenClaw
真实经验:
- 我一开始用LangChain做智能行程规划,快速原型(3天就跑通了)
- 但做到"用户确认后继续规划"这个功能的时候,LangChain做不出来(需要循环/回溯)
- 后来换成LangGraph,多写了200行代码,但功能实现了
- 再后来,要部署到生产环境,需要沙箱隔离,换成了OpenClaw
原则2:需要多Agent协作,用LangGraph或CrewAI
真实经验:
-
我做CSDN文章生成的时候,需要3个Agent协作:
- 规划Agent(决定写什么主题)
- 写作Agent(生成文章)
- 审查Agent(检查文章质量)
-
用LangGraph的
StateGraph,可以定义3个节点,用条件边连接:规划Agent → 写作Agent → 审查Agent ↓ (不通过) 写作Agent(重新写)
原则3:需要安全隔离,用OpenClaw
真实经验:
- 我做OpenClaw Agent的时候,需要让用户自定义工具(Python代码)
- 如果直接用LangChain/LangGraph,用户的代码可以访问文件系统、网络、环境变量,很危险
- 用OpenClaw,用户的代码在沙箱里运行,访问不了宿主机的文件系统
十、你最终会用哪些指标判断这次框架选型是不是成功的?
核心指标
| 指标 | 定义 | 目标值 |
|---|---|---|
| 开发效率 | 从0到跑通原型,需要多少天? | < 7天 |
| 运行性能 | P95延迟是多少? | < 5秒 |
| 维护成本 | 每个月需要多少小时维护? | < 10小时/月 |
| 扩展性 | 增加新工具/新功能,需要多少行代码? | < 100行/功能 |
| 安全性 | 有没有沙箱隔离?权限检查是否完善? | 必须有的 |
真实项目经验
我选LangGraph做智能行程规划的时候,判断选型成功的指标:
1. 开发效率(达标)
- 从0到跑通原型:5天
- 如果用LangChain:3天(但做不出"用户确认后继续规划"这个功能)
- 如果用OpenClaw:10天(生态还在建设,文档少)
2. 运行性能(达标)
- P95延迟:3秒
- 主要耗时:LLM API调用(2秒)+ 高德API调用(0.5秒)+ LangGraph状态管理(0.5秒)
3. 维护成本(基本达标)
- 每个月需要5小时维护(主要是高德API限流,要换百度地图API)
- 如果使用LangChain:维护成本更低(但做不出需要的功能)
- 如果使用OpenClaw:维护成本更高(生态还在建设,经常要自己写MCP Server)
4. 扩展性(达标)
- 增加"用户偏好学习"功能:加了1个Agent节点(PreferenceAgent),150行代码
- 如果用LangChain:要重写整个链(因为需要循环,LangChain不支持)
- 如果用OpenClaw:加1个MCP Server(PreferenceServer),100行代码(更好)
结论:LangGraph的选型是成功的,因为开发效率、运行性能、扩展性都达标,维护成本可接受。
十一、如果前期先用平台,后期迁到自研框架,迁移边界应该怎么划?
直接答案
迁移边界 = 业务逻辑和框架代码的分界线。
真实项目经验
我做CSDN文章生成的时候,迁移路径是:
第一阶段:用LangChain快速原型(0-3个月)
- 业务逻辑和框架代码混在一起
- 代码量少(1000行),跑通核心功能
第二阶段:切换到LangGraph(3-6个月)
- 把业务逻辑抽出来,放到单独的模块
- 框架代码(LangGraph的状态管理、条件边)和业务逻辑分离
代码组织:
# 业务逻辑层(和框架无关,可以复用)
from business_logic.planning import generate_plan
from business_logic.writing import generate_article
from business_logic.review import review_article
# 框架层(LangGraph)
from langgraph.graph import StateGraph
class ArticleState(TypedDict):
topic: str
draft: Optional[str]
review_passed: bool
def create_article_graph():
workflow = StateGraph(ArticleState)
# 框架代码:定义节点和边
workflow.add_node("plan", lambda s: generate_plan(s["topic"])) # 业务逻辑
workflow.add_node("write", lambda s: generate_article(s["draft"])) # 业务逻辑
workflow.add_node("review", lambda s: review_article(s["draft"])) # 业务逻辑
workflow.add_conditional_edges(
"review",
lambda s: "pass" if s["review_passed"] else "fail",
{"pass": END, "fail": "write"}
)
return workflow.compile()
第三阶段:迁移到自研框架(6-12个月)
- 因为业务需要(要支持MCP、要沙箱隔离),LangGraph满足不了
- 自研框架(基于OpenClaw)
- 业务逻辑不用改,因为已经抽出来了
- 只改框架层,把LangGraph的
StateGraph换成自研的AgentGraph
迁移边界的划分原则:
1. 业务逻辑和框架代码必须分离
- 业务逻辑:生成行程、生成文章、调用高德API…
- 框架代码:状态管理、条件边、工具调用…
2. 业务逻辑不能依赖具体框架
- 坏味道:业务逻辑里直接调用LangGraph的API
- 好味道:业务逻辑是纯函数,不依赖任何框架
3. 框架代码要抽象成接口
- 这样换框架的时候,只需要换接口的实现
十二、用过PostgreSQL吗,谈谈你对他的理解?
真实项目经验(值钱版)
我用PostgreSQL做Agent的记忆系统。
使用场景
场景1:存储对话历史
-- 表结构
CREATE TABLE conversations (
id SERIAL PRIMARY KEY,
user_id VARCHAR(255) NOT NULL,
message TEXT NOT NULL,
role VARCHAR(50) NOT NULL, -- 'user' or 'assistant'
timestamp TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
-- 插入对话
INSERT INTO conversations (user_id, message, role)
VALUES ('user_123', '上海有哪些适合拍照的景点?', 'user');
-- 查询对话历史(最近20条)
SELECT * FROM conversations
WHERE user_id = 'user_123'
ORDER BY timestamp DESC
LIMIT 20;
场景2:存储Agent状态
-- 表结构
CREATE TABLE agent_state (
session_id VARCHAR(255) PRIMARY KEY,
state JSONB NOT NULL, -- 用JSONB存储状态(PostgreSQL的特色)
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
-- 插入状态
INSERT INTO agent_state (session_id, state)
VALUES ('session_123', '{
"user_input": "上海3日游",
"draft_plan": {...},
"retry_count": 2
}');
-- 查询状态
SELECT state FROM agent_state WHERE session_id = 'session_123';
为什么用PostgreSQL,不用Redis?
- Redis是内存数据库,重启后数据没了(除非配置持久化)
- PostgreSQL是关系型数据库,数据持久化,支持复杂查询
- PostgreSQL有JSONB类型,可以存半结构化数据(Agent状态通常是JSON)
PostgreSQL的优势
1. 支持JSONB(半结构化数据)
-- 查询JSONB字段(不需要读出来再解析,数据库层面就能查)
SELECT * FROM agent_state
WHERE state->>'user_input' = '上海3日游';
2. 支持向量检索(pgvector插件)
-- 安装pgvector插件
CREATE EXTENSION vector;
-- 表结构(加一个embedding字段,类型是vector)
CREATE TABLE documents (
id SERIAL PRIMARY KEY,
content TEXT NOT NULL,
embedding vector(1536) -- 1536维向量(OpenAI的Embedding维度)
);
-- 向量检索(找和最相似的10篇文档)
SELECT * FROM documents
ORDER BY embedding <-> '[0.1, 0.2, ...]'
LIMIT 10;
这个能力很重要:
- 以前做RAG,需要用专门的向量数据库(Milvus、Chroma)
- 现在用PostgreSQL + pgvector,一个数据库搞定关系型数据 + 向量数据
- 简化架构,不需要维护额外的向量数据库
3. 支持全文检索(Full-Text Search)
-- 添加全文检索列
ALTER TABLE documents ADD COLUMN ts_vector tsvector;
UPDATE documents SET ts_vector = to_tsvector('chinese', content);
-- 全文检索
SELECT * FROM documents
WHERE ts_vector @@ to_tsquery('chinese', '上海 & 拍照 & 景点');
这个能力很重要:
- 做混合检索(向量检索 + 关键词检索),不需要额外的搜索引擎(Elasticsearch)
- PostgreSQL一个数据库搞定
十三、做过大模型微调吗?
真实项目经验(值钱版)
做过,但不推荐。优先用RAG + 记忆系统。
微调项目经验
项目:让LLM记住用户的写作风格
第一版方案:Fine-tuning(失败)
流程:
- 收集用户历史文章(100篇)
- 格式化成训练数据(
{"prompt": "写一篇关于XX的文章", "completion": "用户的历史文章"}) - Fine-tune GPT-4(成本:约3000美元)
- 期望:模型自动用用户的写作风格生成新文章
问题:
- 成本太高:每个用户要Fine-tune一次,100个用户就是30万美元
- 更新慢:用户写了新文章,要重新Fine-tune才能"记住"
- 不可逆:Fine-tune错了(比如混入了低质量文章),要重新训练
第二版方案:RAG + 记忆系统(成功)
流程:
- 把用户历史文章向量化,存进向量数据库
- 生成新文章的时候,先检索用户的历史文章(作为风格参考)
- 把风格参考拼进prompt,让LLM模仿这个风格
代码实现:
def generate_article(user_id, topic):
# 第一步:从记忆系统里取用户的写作风格
user_style = memory.load(user_id, key="writing_style")
# user_style = "简洁、用类比、避免长段落"
# 第二步:从RAG里取用户历史文章(作为风格参考)
similar_articles = vector_db.similarity_search(
query=f"用户{user_id}写的关于{topic}的文章",
k=3
)
# 第三步:拼进prompt
prompt = f"""
写作风格:{user_style}
参考文章:{similar_articles}
请写一篇关于{topic}的文章。
"""
article = llm.invoke(prompt)
return article
效果:
- 成本:几乎为零(只是LLM API调用费)
- 更新:用户写了新文章,立刻向量化存进数据库,下次对话就能用
- 可逆:用户不喜欢这个风格,删除记忆就行
什么时候才需要微调?
只有这三种情况,才考虑微调:
1. 需要让模型学会新能力(比如新语言、新领域知识)
- 例子:让GPT-4学会"用文言文写诗"
- RAG做不到(因为不是"检索知识",而是"改变模型的生成风格")
2. 需要降低推理成本
- 例子:Fine-tune一个小模型(比如Llama-3-8B),效果接近GPT-4
- 推理成本从$0.03/1K tokens(GPT-4)降到$0.0001/1K tokens(Llama-3-8B)
3. 需要离线运行(不能调用外部API)
- 例子:手机端侧AI(不能每次都调用OpenAI API)
- 需要把模型Fine-tune到小尺寸,跑在手机上
十四、谈谈你对LoRA的理解?
真实项目经验(值钱版)
LoRA = 低秩适配,微调大模型的高效方法。
核心原理
问题:Fine-tune大模型,要更新所有参数(比如GPT-4有1.76万亿参数),成本高到离谱。
LoRA的解决方案:
- 不改模型的原始参数
- 在模型旁边加"小插件"(低秩矩阵)
- Fine-tune只更新"小插件"的参数(比如几百万参数,比1.76万亿少多了)
类比:
- 全量Fine-tune = 重新装修房子(贵、慢、不可逆)
- LoRA = 贴墙纸(便宜、快、可移除)
代码实现
from peft import LoraConfig, get_peft_model
from transformers import AutoModelForCausalLM
# 加载预训练模型
model = AutoModelForCausalLM.from_pretrained("gpt2")
# 配置LoRA
lora_config = LoraConfig(
r=8, # 低秩矩阵的秩(越小,参数越少,训练越快,但效果可能差)
lora_alpha=32,
target_modules=["c_attn"], # 对哪些模块应用LoRA
lora_dropout=0.1,
)
# 应用LoRA
model = get_peft_model(model, lora_config)
# 训练(只更新LoRA参数,不更新原始模型参数)
trainer.train()
# 保存(只保存LoRA参数,不保存原始模型参数)
model.save_pretrained("./lora_weights") # 只有几MB(原始模型有几GB)
真实项目经验
我做让用户自定义写作风格的时候,用LoRA Fine-tune了一个小模型(Llama-3-8B):
流程:
- 收集用户历史文章(100篇)
- 用LoRA Fine-tune Llama-3-8B(只更新LoRA参数,几百万参数)
- 保存LoRA权重(几MB)
- 推理的时候,加载LoRA权重(插到Llama-3-8B上)
效果:
- 成本:约100美元(vs 全量Fine-tune需要3000美元)
- 效果:生成的文章,风格和用户的真实文章相似度85%
- 速度:Fine-tune只需2小时(vs 全量Fine-tune需要2天)
但最后还是放弃了,改用RAG + 记忆系统(因为更新更灵活)。
十五、你们公司的agent上线流程是哪样的,是用到docker吗?
真实项目经验(值钱版)
上线流程:本地测试 → Staging环境 → 灰度发布 → 全量发布。用Docker。
完整上线流程
第一步:本地测试
# 本地跑Agent
python run_agent.py --topic "AI应用开发面试题解析"
# 本地测试通过后,构建Docker镜像
docker build -t csdn-article-agent:v1.0 .
第二步:Staging环境
# 部署到Staging环境(内网,只有员工能访问)
docker run -d \
--name csdn-agent-staging \
-e OPENAI_API_KEY=xxx \
-e CSDN_COOKIE=xxx \
csdn-article-agent:v1.0
# Staging环境测试
curl http://staging-internal.csdn-agent.com/generate \
-d '{"topic": "AI应用开发面试题解析"}'
第三步:灰度发布
# 灰度发布(10%流量)
# 用Kubernetes的Deployment,设置replicas=10,其中1个是新版本
kubectl apply -f deployment-v1.1.yaml
# deployment-v1.1.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: csdn-agent-v1.1
spec:
replicas: 1 # 10%流量(总共10个replicas,1个是新版本)
selector:
matchLabels:
app: csdn-agent
version: v1.1
template:
metadata:
labels:
app: csdn-agent
version: v1.1
spec:
containers:
- name: agent
image: csdn-article-agent:v1.1
第四步:全量发布
# 灰度发布24小时,没有异常,全量发布
kubectl scale deployment csdn-agent-v1.0 --replicas=0 # 旧版本缩容到0
kubectl scale deployment csdn-agent-v1.1 --replicas=10 # 新版本扩容到10
为什么用Docker?
1. 环境一致性
- 本地测试用Docker,Staging用Docker,生产用Docker
- 不会出现"本地能跑,生产跑不了"的问题
2. 快速回滚
- 如果新版本有问题,一键回滚到旧版本
kubectl rollout undo deployment csdn-agent-v1.1
3. 资源隔离
- 每个Agent跑在独立的容器里,互相不影响
- 一个Agent挂了,不会拖垮其他Agent
4. 弹性伸缩
- 流量高峰期,自动扩容(比如从10个replicas扩容到100个)
- 流量低谷期,自动缩容(从100个缩容到10个)
总结:AI应用开发的核心能力模型
这15个问题,覆盖了AI应用开发的核心能力:
| 能力维度 | 涵盖问题 | 核心要求 |
|---|---|---|
| RAG系统 | 1, 2 | 不是背书,而是真的用过RAG,知道优势、局限性、怎么解决检索精度问题 |
| Agent运维 | 3 | 知道要监控哪些指标,会排查生产事故 |
| MCP协议 | 4, 5, 6, 7, 8 | 理解MCP的跨平台兼容原理,会开发MCP Server,知道MCP和沙箱的关系 |
| 框架选型 | 9, 10, 11 | 真的用过多个框架,知道优缺点,会判断选型是否成功,知道迁移边界怎么划 |
| 数据库与微调 | 12, 13, 14 | 真的用过PostgreSQL(最好是pgvector),真的做过微调(最好是LoRA) |
| 部署运维 | 15 | 知道上线流程,会用Docker/Kubernetes |
面试准备建议:
-
不要背概念
- 面试官听得出你是不是背的
- 用"我之前做XX项目的时候,遇到过XX问题,我是这么解决的"来回答
-
准备2-3个深度项目
- 不要列10个项目(面试官问哪个你都说不清楚)
- 列2-3个,每个都能讲30分钟(背景、技术选型、踩坑、解决方案、收获)
-
关注技术趋势
- 比如MCP协议(2024年底才出),如果你知道并且用过,加分很多
- 比如PostgreSQL + pgvector(2024年流行),如果你用过,加分很多
参考资料
- RAG系统设计:https://www.anthropic.com/research/retrieval-augmented-generation
- MCP协议官方文档:https://modelcontextprotocol.io/
- LangGraph vs LangChain对比:https://langchain-ai.github.io/langgraph/concepts/langgraph/
- PostgreSQL pgvector插件:https://github.com/pgvector/pgvector
- LoRA微调原理:https://arxiv.org/abs/2106.09685
作者注:这篇文章的每一个答案,都来自真实的项目踩坑经验。如果你也在准备AI应用开发的面试,希望能帮你少走弯路。如果有问题,欢迎在评论区讨论。
标签:#快手面试 #AI应用开发 #RAG #MCP #Agent运维 #框架选型 #PostgreSQL #LoRA #Docker部署
openEuler 是由开放原子开源基金会孵化的全场景开源操作系统项目,面向数字基础设施四大核心场景(服务器、云计算、边缘计算、嵌入式),全面支持 ARM、x86、RISC-V、loongArch、PowerPC、SW-64 等多样性计算架构
更多推荐



所有评论(0)