快手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)

问题

  1. 上下文长度限制:最新动态可能有几万字,放不进上下文
  2. 信息过时:每周都有新动态,每次都要改prompt
  3. 无关信息干扰: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描述里没有
  • 检索结果返回的是"外滩""田子坊"这些热门景点,不是小众景点

为什么检索失败?

  1. 语义鸿沟:用户说的"小众",POI描述里写的是"安静"“人少”
  2. 多条件组合:"小众+适合拍照+免费"是三个条件的组合,向量检索擅长单条件,多条件组合容易失败
  3. 长尾覆盖差:小众景点在数据集中占比低,向量检索优先返回高频内容

解决方案(我试过的三种):

方案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,成本高

面试时可以说的点

  1. 优势:让LLM从"闭卷考试"变成"开卷考试",知识实时更新,成本远低于Fine-tuning
  2. 局限性:检索不到就生成不出来,多条件组合查询容易失败,长尾知识覆盖差
  3. 解决方案:查询改写、混合检索、微调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%,但摘要可能丢失细节。

我的最终方案(生产环境在用):

混合策略

  1. 第一轮:向量检索召回20篇
  2. 第二轮:重排序取前10篇
  3. 第三轮:对前10篇做摘要压缩,每篇压缩到500字
  4. 最终:把压缩后的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数据库?”

检索回来的文档:

  1. “Python数据库连接池的原理”(语义相关,但不是用户要的)
  2. “MySQL索引优化指南”(语义相关,但不是用户要的)
  3. “用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  # 防止提权

两层防护

  1. MCP层:检查Agent是否有权限调用工具
  2. 容器层:即使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(失败)

流程

  1. 收集用户历史文章(100篇)
  2. 格式化成训练数据({"prompt": "写一篇关于XX的文章", "completion": "用户的历史文章"}
  3. Fine-tune GPT-4(成本:约3000美元)
  4. 期望:模型自动用用户的写作风格生成新文章

问题

  1. 成本太高:每个用户要Fine-tune一次,100个用户就是30万美元
  2. 更新慢:用户写了新文章,要重新Fine-tune才能"记住"
  3. 不可逆:Fine-tune错了(比如混入了低质量文章),要重新训练

第二版方案:RAG + 记忆系统(成功)

流程

  1. 把用户历史文章向量化,存进向量数据库
  2. 生成新文章的时候,先检索用户的历史文章(作为风格参考)
  3. 把风格参考拼进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):

流程

  1. 收集用户历史文章(100篇)
  2. 用LoRA Fine-tune Llama-3-8B(只更新LoRA参数,几百万参数)
  3. 保存LoRA权重(几MB)
  4. 推理的时候,加载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

面试准备建议

  1. 不要背概念

    • 面试官听得出你是不是背的
    • 用"我之前做XX项目的时候,遇到过XX问题,我是这么解决的"来回答
  2. 准备2-3个深度项目

    • 不要列10个项目(面试官问哪个你都说不清楚)
    • 列2-3个,每个都能讲30分钟(背景、技术选型、踩坑、解决方案、收获)
  3. 关注技术趋势

    • 比如MCP协议(2024年底才出),如果你知道并且用过,加分很多
    • 比如PostgreSQL + pgvector(2024年流行),如果你用过,加分很多

参考资料

  1. RAG系统设计:https://www.anthropic.com/research/retrieval-augmented-generation
  2. MCP协议官方文档:https://modelcontextprotocol.io/
  3. LangGraph vs LangChain对比:https://langchain-ai.github.io/langgraph/concepts/langgraph/
  4. PostgreSQL pgvector插件:https://github.com/pgvector/pgvector
  5. LoRA微调原理:https://arxiv.org/abs/2106.09685

作者注:这篇文章的每一个答案,都来自真实的项目踩坑经验。如果你也在准备AI应用开发的面试,希望能帮你少走弯路。如果有问题,欢迎在评论区讨论。

标签:#快手面试 #AI应用开发 #RAG #MCP #Agent运维 #框架选型 #PostgreSQL #LoRA #Docker部署

Logo

openEuler 是由开放原子开源基金会孵化的全场景开源操作系统项目,面向数字基础设施四大核心场景(服务器、云计算、边缘计算、嵌入式),全面支持 ARM、x86、RISC-V、loongArch、PowerPC、SW-64 等多样性计算架构

更多推荐