1. Python GIL 是什么?项目中如何使用的
回答
GIL (Global Interpreter Lock),全局解释器锁
- CPython 特有的锁,同一时刻一个 Python 进程内只能有一个线程执行 Python 字节码。
- GIL 不会锁住 IO,IO 阻塞的时候 GIL 会释放;但是 CPU 密集型计算,多线程无法利用多核。
- 两个结论:
- IO 密集型任务(网络请求:调用 LLM、Milvus、Redis、HTTP 接口):多线程 /asyncio 异步可以获得收益,等待网络的时候释放 GIL。
- CPU 密集(大量文本分片、Embedding 计算):多线程没用,要用多进程,绕过 GIL。
✅ 项目场景(你的 Agent 项目)
- FastAPI 里的接口请求,属于IO 密集:调用 LLM、Milvus、Redis、数据库,大量时间在等待网络 IO,所以我们用异步 async。IO 等待期间释放 GIL,并发处理更多请求。
- 文档流水线:文档分片、文本清洗、Embedding 推理是 CPU 密集,这部分单独开多进程池处理,绕开 GIL,不占用 web 服务主进程。
坑点:不要在 FastAPI 同步接口里跑大量 CPU 计算,会阻塞整个 worker。
代码示例(多进程处理文档分片,规避 GIL)
from multiprocessing import Pool
def split_document(text: str):
"""CPU密集:文档分片,放到子进程执行,绕开GIL"""
chunks = []
# 标题感知分片逻辑
return chunks
if __name__ == "__main__":
texts = ["文档文本1", "文档文本2"]
with Pool(processes=4) as pool:
result = pool.map(split_document, texts)
口述一句话总结:
GIL 是 CPython 全局解释锁,限制单进程同一时刻只有一个线程跑 Python 字节码。我们 Agent 服务,IO 密集的 LLM/Milvus 网络请求用异步;CPU 密集的文档预处理、embedding 向量化,用多进程绕过 GIL。
2. Python 中间件知道些什么,项目怎么使用
回答
Python 中间件,在 FastAPI 里就是请求 / 响应的拦截钩子 ,在路由处理前后执行通用逻辑。
FastAPI 中间件基于Starlette中间件体系。
常见中间件类型:
- 内置:CORSMiddleware(跨域)
- 自定义中间件:全链路 traceId 注入、日志埋点、请求耗时统计、全局异常捕获、鉴权校验。
✅ 项目场景(你的 Agent 项目)
我们自定义了一个Trace 中间件:
- 请求进来生成 traceId,注入 request.state,向下透传给所有下游调用(Milvus、Redis、LLM 请求)
- 记录请求开始时间,接口返回时计算耗时,打印结构化日志
- 统一捕获全局未处理异常,包装成统一错误返回体
注意:中间件分同步中间件 和
@asynccontextmanager异步中间件,异步项目必须用异步中间件。
代码示例
from fastapi import Request
from starlette.middleware.base import BaseHTTPMiddleware
import uuid
import time
class TraceMiddleware(BaseHTTPMiddleware):
async def dispatch(self, request: Request, call_next):
# 请求进入:生成traceId
trace_id = str(uuid.uuid4())
request.state.trace_id = trace_id
start_time = time.time()
response = await call_next(request)
# 返回前:记录耗时
cost = time.time() - start_time
response.headers["X-Trace-Id"] = trace_id
print(f"trace={trace_id}, path={request.url.path}, cost={cost}")
return response
# 注册到app
# app.add_middleware(TraceMiddleware)
# app.add_middleware(CORSMiddleware, allow_origins=["*"], ...)
口述总结:
FastAPI 基于 Starlette 中间件,项目中用到 CORS 内置中间件,自定义 Trace 中间件做全链路追踪、耗时埋点,统一日志,traceId 贯穿 Agent 整个 RAG 和 LLM 调用链路。
3. FastAPI 工具函数有使用嘛,项目中使用场景
回答
FastAPI 工具函数,一般指Depends依赖注入函数(最核心),还有 HTTPException、BackgroundTasks 等。
依赖注入 Depends:抽取可复用逻辑,自动注入到路由函数。
✅ 项目场景 Agent 项目
- 依赖函数
get_tenant_context:从 request.state 读取 token 解析出来的 tenantId,校验租户状态,注入到 Agent 接口,所有 RAG / 工具调用路由复用。 - 数据库会话依赖:get_db,获取 PG 连接会话。
- BackgroundTasks 后台任务:异步写入审计日志,不阻塞主请求(Agent 调用 LLM 成功后,把日志丢后台任务入库)。
代码示例
from fastapi import Depends, BackgroundTasks, Request, HTTPException
from pydantic import BaseModel
async def get_tenant_ctx(request: Request):
"""依赖函数:获取租户上下文,所有Agent接口复用"""
tenant_id = request.state.tenant_id
if not tenant_id:
raise HTTPException(status_code=401, detail="tenant invalid")
return tenant_id
@app.post("/agent/chat")
async def agent_chat(
req: BaseModel,
tenant_id: str = Depends(get_tenant_ctx),
bg: BackgroundTasks = BackgroundTasks()
):
result = await run_agent(req.query, tenant_id)
# 后台任务,异步写审计日志,不阻塞主链路
bg.add_task(save_audit_log, tenant_id, result)
return result
口述总结:大量使用 Depends 依赖函数抽取租户上下文、数据库会话;BackgroundTasks 后台任务做审计日志异步落库,不阻塞 Agent 主请求链路。
4. FastAPI 异步和同步使用场景;Agent 服务应该异步还是同步
回答
区分场景
- async def 异步接口 :适合 IO 密集任务。等待网络(LLM、Milvus、Redis、DB),await 释放事件循环,worker 可以处理其他请求。
适用:Agent 对话接口、RAG 检索、调用 LLM、向量库查询。
✅ Agent 主链路推荐异步,Agent 核心链路全部是 IO 等待:LLM 调用、Milvus 检索、Redis 会话读取、PostgreSQL 查询,大量 IO 等待,异步可以提升并发。
- def 同步接口 :适合 CPU 密集任务,或者调用同步第三方 SDK(部分老 SDK 不支持 async)。
同步接口 FastAPI 会自动丢到线程池执行;不适合大量并发 IO 场景,线程池上限有限。
注意坑:异步函数内部不能直接调用同步阻塞 SDK (同步 Redis、同步 Milvus 客户端),会卡死事件循环;要用 async 版本 SDK,或者用
asyncio.to_thread()包装同步阻塞调用。
代码示例(同步 SDK 放到线程池执行,不阻塞事件循环)
import asyncio
import milvus # 假设milvus是同步SDK
async def rag_search(tenant_id:str, query:str):
# 同步Milvus调用,放到线程池
res = await asyncio.to_thread(milvus.search, tenant_id, query)
return res
口述总结:Agent 服务主链路全部用 async 异步接口,因为大量 IO 等待(LLM、向量库、Redis),提高并发;如果调用老的同步 SDK,使用 asyncio.to_thread 丢入线程池,防止阻塞事件循环;纯 CPU 密集的文档预处理,放到独立进程,不放在 web 接口里。
5. FastAPI 迭代器和生成器区别、使用场景
面试官实际想问 Python 原生迭代器 / 生成器,在 FastAPI SSE / 流式输出场景(Agent 大模型流式返回)。
回答
- 迭代器 (Iterator) :实现
__iter__+__next__,保存状态,逐个返回元素,手动迭代。 - 生成器 (Generator) :用
yield关键字,是迭代器的一种简化写法,函数执行到 yield 暂停,下次迭代从断点继续,自动维护上下文状态,内存友好,不用一次性把全部数据加载到内存。
✅ 项目场景:Agent SSE 流式输出(重点!)
SSE 流式返回 LLM 的 token 流,用异步生成器 async def + yield,一段一段吐出 LLM 返回的内容,不需要等大模型全部生成完一次性返回,前端实时展示文字。
代码示例:异步生成器用于 SSE
import asyncio
async def llm_stream_generator(query: str):
"""异步生成器,每次yield一段token"""
for chunk in await call_llm_stream(query):
yield f"data: {chunk}\n\n"
await asyncio.sleep(0.01)
@app.get("/agent/stream")
async def stream_chat():
from fastapi.responses import StreamingResponse
return StreamingResponse(llm_stream_generator("用户提问"), media_type="text/event-stream")
口述总结:生成器是迭代器的简化实现,基于 yield。项目中 SSE 流式输出 LLM token,使用异步生成器,分段返回数据,不需要把全部回答加载进内存,适合 Agent 实时对话场景。
6. FastAPI 函数装饰器有哪些,怎么用
回答
FastAPI 常用装饰器:
@app.get / @app.post / @app.put:注册路由,最基础@app.websocket:websocket 路由- 自定义装饰器:用来做接口拦截、权限校验、日志统计(注意:异步接口要写异步装饰器)
补充:Python 原生装饰器原理:包装函数,在函数执行前后插入逻辑。
代码示例:自定义装饰器,统计接口耗时
import time
from functools import wraps
from fastapi import Request
def api_timer(func):
@wraps(func)
async def wrapper(request: Request, *args, **kwargs):
start = time.time()
res = await func(request, *args, **kwargs)
print(f"route cost: {time.time() - start}")
return res
return wrapper
@app.post("/agent/chat")
@api_timer # 自定义装饰器
async def chat(request: Request):
return {"msg":"ok"}
口述总结:内置@app.get/post路由装饰器;项目写自定义异步装饰器统计接口耗时。但大部分通用逻辑,优先用中间件 / Depends 依赖,而不是装饰器。因为装饰器不好统一管理,新增路由容易忘记加。
7. 解析式下python中@的用法?
回答
@是一个装饰器,针对函数,起调用传参的作用。 有修饰和被修饰的区别,'@function'作为一个装饰器,用来修饰紧跟着的函数(可以是另一个装饰器,也可以是函数定义)。
bash
复制代码
def funA(desA):
print("It's funA")
def funB(desB):
print("It's funB")
@funB
@funA
def funC():
print("It's funC")
复制代码
比如上面输出的是
bash
It's funA
It's funB
@funB 修饰装饰器@funA,@funA 修饰函数定义def funC(),将funC()赋值给funA()的形参,再将funA(funC())赋值给funB()。 执行的时候由上而下,先定义funA、funB,然后运行funB(funA(funC()))。
此时desA=funC(),然后funA()输出'It's funA';desB=funA(funC()),然后funB()输出'It's funB'。
8. Embedding 用什么模型,如何使用
回答
贴合项目:私有化 / API 调用 Embedding 模型,比如 bge-large-zh,或者厂商 embedding 接口。
- 模型:BGE-Large-zh ,中文效果好,适合金融 / 知识库文档向量化。
使用流程:
- 文档分片后,把 chunk 文本送入 Embedding 模型,输出 float 向量数组。
- 入库 Milvus;用户 query 也用同一个 Embedding 模型生成向量,做向量检索。
重点:Query 和文档必须用同一个 Embedding 模型,否则向量空间不一致,召回失效。
代码示例
from sentence_transformers import SentenceTransformer
# 加载bge-large-zh
model = SentenceTransformer("BAAI/bge-large-zh")
def get_embedding(text:str):
vec = model.encode(text, normalize_embeddings=True)
return vec.tolist()
chunk_text = "监管规则文档片段"
vector = get_embedding(chunk_text)
# 写入Milvus
# milvus.insert(vector=vector, chunk_text=chunk_text, tenant_id="xxx")
口述:项目使用 BGE-Large-zh 作为 Embedding 模型,文档分片和用户 query 都用同一个模型生成向量;向量做归一化,存入 Milvus;检索时用户问题生成向量做相似度匹配。
可选:如果是调用线上 API,就换成 http 请求调用 embedding 接口。
9. Agent Skills/Tools/MCP 区别,项目用过哪些,代码解释如何调用?
回答
概念区分
- Tool(工具 / Function Call)
LLM 原生能力:LLM 识别意图,输出结构化函数调用参数,由程序执行函数,把结果丢回给 LLM 继续推理 。
本质:LLM 驱动的函数调用,OpenAI function call / Anthropic tool call 都属于这一类。
粒度:单个可调用函数,比如查询工单、计算风险分。
- Skill(技能)
一组 Tool + 执行逻辑 + 提示词 + 业务编排 ,封装成一个完整能力。
Skill 是更高一层的业务抽象,一个 Skill 可以包含多个 Tool、多轮工具调用、前置校验、结果后处理。
举例:【工单风险评估 Skill】,内部会连续调用
query_work_order、risk_score_calculate两个 tool,并且内置业务判断逻辑,这整套打包叫 Skill。
简单一句话区分:
- Tool = 最小可调用函数单元
- Skill = 业务能力包,由多个 Tool + 编排逻辑组成
- MCP(Model Context Protocol)
MCP 是模型上下文协议,标准化协议,用来让 Agent 安全、标准化调用外部服务 / 资源(文件、数据库、API、第三方系统)。
和普通 Function Call 最大差别:
普通 Tool:工具定义、调用逻辑、参数解析全部写在你自己 Agent 代码里面 ,自定义实现,没有统一标准。
MCP:遵循 MCP 协议,Agent 作为客户端,连接 MCP Server;MCP Server 向外暴露可调用工具列表,工具能力由外部 MCP 服务提供,Agent 不需要写每个工具的底层实现 。
优势:工具可插拔,新增外部能力不用改 Agent 核心代码;支持权限、沙箱隔离。
局限:项目中如果是内部自研 Agent,一般不会上 MCP,MCP 适合对接大量异构外部系统。
✅ 你的项目场景(金融质检 Agent)
- 没有引入 MCP(可以直接说:项目当时没有接入 MCP,MCP 属于较新的标准,我们自研工具适配器做函数调用)
- 一共封装 3 个 Tool(最小函数单元)
query_work_order:查询工单详情risk_score_calculate:风险打分update_task_status:更新质检任务状态
- 封装 1 个业务 Skill:
quality_risk_audit_skill(质检风险评估 Skill),内部编排调用上面 3 个 Tool,加前置参数校验、结果格式化。
一句话口述总结:
Tool 是最小的可调用函数;Skill 是把多个 Tool 加上业务逻辑封装成一个完整业务能力;MCP 是标准化协议,用来让 Agent 对接外部服务的工具,我们项目自研 Agent 没有接入 MCP,自己实现 Tool 适配器。
步骤 1:定义工具描述(给 LLM 识别用的 JSON Schema)
from pydantic import BaseModel, Field
import json
# ========== Tool定义(最小调用单元) ==========
class QueryWorkOrderReq(BaseModel):
order_id: str = Field(description="工单唯一ID")
def query_work_order(order_id: str):
"""Tool:查询工单信息,同步调用业务服务"""
# 调用业务微服务http接口
# resp = requests.get(f"http://biz-api/order/{order_id}")
return {"order_id": order_id, "amount": 10000, "create_time": "2026-09-01"}
def risk_score_calculate(order_data: dict):
"""Tool:风险打分"""
score = 0
if order_data["amount"] > 5000:
score = 70
return {"risk_score": score}
def update_task_status(task_id: str, status: str):
"""Tool:更新任务状态,写操作"""
return {"task_id": task_id, "new_status": status}
# 工具元信息,传给LLM,用于function call识别
tool_list = [
{
"type": "function",
"function": {
"name": "query_work_order",
"description": "查询工单详情,输入工单ID,返回工单基础信息",
"parameters": QueryWorkOrderReq.model_json_schema()
}
},
{
"type": "function",
"function": {
"name": "risk_score_calculate",
"description": "根据工单数据计算风险分数",
"parameters": {
"type": "object",
"properties": {"order_data": {"type": "object"}}
}
}
},
{
"type": "function",
"name": "update_task_status",
"description": "更新质检任务状态",
"parameters": {
"type": "object",
"properties": {
"task_id": {"type": "string"},
"status": {"type": "string"}
},
"required": ["task_id", "status"]
}
}
]
步骤 2:Tool 适配器(统一分发,Agent 核心引擎不感知底层实现,就是你前面架构讲的 ToolAdapter)
# Tool适配器:统一路由,根据LLM返回的工具名称,执行对应函数
class ToolAdapter:
def __init__(self):
self.tool_map = {
"query_work_order": query_work_order,
"risk_score_calculate": risk_score_calculate,
"update_task_status": update_task_status
}
async def run_tool(self, tool_name: str, tool_args: dict):
"""执行工具,统一捕获异常,标准化返回"""
if tool_name not in self.tool_map:
return {"error": f"不存在工具{tool_name}"}
func = self.tool_map[tool_name]
# 如果是同步函数,放到线程池,不阻塞async事件循环
result = await asyncio.to_thread(func,** tool_args)
return result
步骤 3:Skill 封装:质检风险评估 Skill(多个 Tool 编排)
# Skill:业务能力封装,编排多轮tool调用
class QualityRiskAuditSkill:
def __init__(self, adapter: ToolAdapter):
self.adapter = adapter
self.max_round = 3 # 限制最大tool调用轮次,防止死循环
async def run(self, task_id: str, order_id: str):
"""Skill主流程:一整套业务编排"""
# Step1:调用tool查询工单
order_info = await self.adapter.run_tool("query_work_order", {"order_id": order_id})
if "error" in order_info:
return order_info
# Step2:调用tool计算风险分
risk_res = await self.adapter.run_tool("risk_score_calculate", {"order_data": order_info})
# Step3:调用tool更新任务状态
await self.adapter.run_tool("update_task_status", {"task_id": task_id, "status": "ai_audited"})
return {
"order_info": order_info,
"risk_result": risk_res
}
步骤 4:Agent 主逻辑:LLM 判断要不要调用 Tool/Skill
async def agent_run(user_query: str, session_id: str):
adapter = ToolAdapter()
audit_skill = QualityRiskAuditSkill(adapter)
# 1. 调用LLM,传入工具列表,让LLM判断意图
llm_resp = await call_llm(messages=[{"role":"user","content":user_query}], tools=tool_list)
# 2. LLM返回工具调用指令
if llm_resp.get("tool_calls"):
for tool_call in llm_resp["tool_calls"]:
t_name = tool_call["function"]["name"]
t_args = json.loads(tool_call["function"]["arguments"])
# 执行tool
tool_result = await adapter.run_tool(t_name, t_args)
# 把工具执行结果塞回消息列表,再次交给LLM做推理
messages.append({
"role": "tool",
"tool_call_id": tool_call["id"],
"content": json.dumps(tool_result)
})
# 二次调用LLM,结合工具返回结果生成最终回答
final_resp = await call_llm(messages)
return final_resp["content"]
else:
# 不需要调用工具,直接返回LLM回答
return llm_resp["content"]
面试官高频追问(配套回答)
Q1:什么时候用 Tool,什么时候封装成 Skill?
回答:
Tool 是最小单元,单一原子操作,只做一件事;
如果业务需要固定顺序连续调用多个 Tool,带前置校验、后置结果组装、固定业务规则 ,就封装成 Skill。
比如风险评估必须:查工单→算分→更新状态,固定流程,封装成 Skill,Agent 直接调用这个 Skill,不用 LLM 反复决定每一步工具。
Q2:MCP 和你这个自研 Tool 适配器的差异?
回答:
自研 Tool 适配器:工具定义、执行代码全部在项目内部,适合内部业务 API,简单可控。缺点:新增工具需要改代码、重新发布服务。
MCP 是标准协议,Agent 作为客户端去连接独立 MCP Server,MCP 服务向外暴露工具。新增工具只需要在 MCP 服务配置,不需要修改 Agent 核心代码,适合对接外部异构系统、文件、数据库,做插件化 Agent。
我们项目是内部业务质检,工具数量少,迭代可控,所以自研适配器,没有引入 MCP。
Q3:Tool 调用死循环怎么避免?
回答:
两层防护:
- 全局设置最大工具调用轮次,超过直接终止;
- 每次拿到工具返回结果,Prompt 引导 LLM 判断信息是否足够回答用户,不需要继续调用工具。
Q4:Tool 返回结果很大怎么办?
回答:
对 tool 返回结果做摘要、截断,只保留 LLM 推理需要的关键字段,减少 token 消耗,避免 prompt 超长。
Q5:Tool 调用存在并发 / 数据脏写问题怎么处理?
回答:
写类型 tool(update_task_status)增加幂等 key,防止 LLM 重复触发工具调用,重复更新状态。
10. Agent 幻觉如何减少,如何提高准确性,举例场景
回答
幻觉本质:LLM 在没有足够参考信息时编造内容。多层策略组合:
- RAG 层:分片优化(标题感知分片)、多路召回 + 重排,提升参考文档质量,把正确的上下文给到 LLM。
- Prompt 约束 :强制要求模型只能基于提供的参考文档回答;无相关信息直接回答不知道,禁止编造;输出附带引用 chunk_id 溯源。
- 结果校验层:对输出结果做校验,引用分片相似度低于阈值直接拒绝生成答案。
- 业务校验:金融质检场景,AI 输出仅作为辅助,人工复核。
- 拒绝编造策略:召回分数低于阈值,直接返回知识库无相关信息,不送入 LLM。
✅ 场景例子(金融质检)
当 Agent 判断录音是否违规,必须基于召回的监管文档片段;Prompt 强制要求:如果参考文档没有对应的监管条款,不能自行编造违规结论,同时输出引用的分片 ID 用于溯源。
代码示例(Prompt 约束模板)
system_prompt = """
你只能基于【参考文档】回答用户问题。
如果参考文档没有对应信息,直接回复"没有找到相关依据",禁止编造任何规则。
输出结果附带引用的分片ID。
参考文档:{context}
"""
口述总结:幻觉是组合方案,RAG 提升召回质量,Prompt 强约束,低相似度召回直接拦截,输出强制溯源;金融场景 AI 结果还要人工复核。
11. Agent 多轮对话怎么设计
回答
整体思路:会话 ID 隔离,上下文分段存储,长短记忆分离
- 生成唯一 session_id,绑定租户 + 用户,作为会话唯一标识。
- 短期记忆(Redis):存储最近多轮对话(user、assistant、tool 调用记录),设置 TTL;每次请求读取历史,拼入 prompt。
- 上下文窗口管理:对话累积过长时,做摘要压缩,防止 token 超限。
- 长期记忆(PostgreSQL):会话全量记录异步持久化,用于审计,不会全部塞到 prompt。
- 会话隔离:不同 session、不同租户完全隔离。
存储结构:Redis key session:{tenant_id}:{session_id},value 是 json 数组,每一条包含 role, content, tool_calls。
示例伪代码
# 读取历史
history = await redis.get(f"session:{tenant_id}:{session_id}")
messages = json.loads(history)
messages.append({"role":"user", "content":user_query})
# 调用LLM
resp = await llm.chat(messages=messages)
messages.append({"role":"assistant", "content":resp})
# 写回Redis
await redis.set(f"session:{tenant_id}:{session_id}", json.dumps(messages), ex=3600)
口述总结:多轮对话使用 sessionId 隔离,短期对话放 Redis,带过期时间;每次请求读取历史拼接消息;上下文过长自动摘要压缩;会话结束异步写入 PG 审计。
12. Agent LLM 超时如何处理,如何优化
回答
处理策略(分层)
- 请求层超时设置:LLM http 客户端设置 timeout,区分连接超时、读取超时。
- 重试策略:网络抖动导致的短暂超时,做有限次数重试(指数退避;业务敏感场景不能无限重试,防止重复调用 LLM)。
- 熔断降级:滑动窗口统计 LLM 接口失败率,连续失败达到阈值,触发熔断,不再调用 LLM,直接返回降级结果。
- 链路超时控制:FastAPI、Nginx 设置最大超时,防止请求长时间挂住占用 worker。
优化手段
- 减少单次 prompt token 数量:上下文摘要、缩短参考分片。
- 开启 LLM 流式 SSE,提前返回 token,前端逐步渲染,用户感知等待变短。
- 缓存高频相同 query 结果,避免重复调用 LLM。
代码示例:aiohttp 客户端超时 + 重试
import aiohttp
from tenacity import retry, stop_after_attempt, wait_exponential
@retry(stop=stop_after_attempt(2), wait=wait_exponential(multiplier=1, min=1, max=3))
async def call_llm(prompt:str):
timeout = aiohttp.ClientTimeout(total=15) # 总超时15s
async with aiohttp.ClientSession(timeout=timeout) as session:
resp = await session.post("llm-api", json={"prompt":prompt})
return await resp.json()
口述总结:设置客户端超时;网络抖动做有限次指数退避重试;熔断机制,LLM 大面积故障直接降级;优化上做上下文压缩、SSE 流式输出、高频问答缓存。
13. Agent SSE 结构是什么,项目怎么使用;如果存储 SSE 流到 Redis,Redis 用什么结构
回答
SSE (Server-Sent Events) 结构
SSE 是服务端单向推送 的 http 协议,基于长连接;响应头Content-Type: text/event-stream。
消息格式:
data: 消息内容\n\n
可以带 event、id 字段。适合 LLM 流式输出,一段一段推送 token 给前端。
✅项目使用:Agent 对话接口,StreamingResponse + 异步生成器,实时返回 LLM 流式 token,前端打字机效果。
如果要把 SSE 流存入 Redis:选 List(列表)结构
原因:SSE 是有序的流式分段,一段一段按顺序产生。
- key:
sse_stream:{session_id} - 每次产生一个 chunk,
LPUSH / RPUSH追加到 list。 - 读取按顺序 range 获取所有片段,重建完整回答。
不适合 String:流式分段不断追加,字符串会不断扩容,性能差。
不适合 Hash:hash 适合 k-v 键值对,不适合有序流。
代码示例
# 写入每一段sse chunk到redis list
await redis.rpush(f"sse_stream:{session_id}", json.dumps({"chunk":token_chunk}))
# 读取全部片段
chunks = await redis.lrange(f"sse_stream:{session_id}", 0, -1)
口述总结:SSE 是 http 长连接,服务端单向推送,固定 data:xxx 格式;项目用 StreamingResponse + 异步生成器实现 LLM 打字机流式返回。保存 SSE 分段流到 Redis,选用 List 有序列表,RPUSH 追加每一段 token 分片。
追加高频面试题(Python + FastAPI + Agent/RAG,继续准备)
14. FastAPI 的 Depends 和 BackgroundTasks 区别?
Depends:请求执行前 / 中 ,注入依赖,同步 / 异步,用来获取上下文、db 会话。
BackgroundTasks:响应返回给前端之后 ,后台执行任务,不阻塞主请求。
项目场景:BackgroundTasks 用来异步写审计日志,Depends 用来获取租户上下文。
15. 异步代码里面如果写 time.sleep () 会发生什么?怎么替换?
time.sleep () 是阻塞调用,会阻塞整个事件循环。异步代码必须用await asyncio.sleep()。
16. 什么是 FastAPI Pydantic,项目怎么用?
Pydantic 做请求体、参数校验,自动类型转换。Agent 项目:入参校验、Function-Call 参数 schema 定义,LLM 输出结构化解析。
from pydantic import BaseModel
class AgentChatReq(BaseModel):
query: str
session_id: str
17. RAG 多路召回是什么,BM25 和向量检索优缺点
向量召回:语义匹配;对字面不一样、语义相同的问题召回好;缺点:依赖 Embedding,计算量大。
BM25 关键词检索:字面关键词匹配;对专有名词效果好;无法理解语义。
多路召回:两个结果拿到,重排序融合,取 topN,互补。
18. LLM 流式输出 SSE 和 websocket 区别?
SSE:单向,http 长连接,服务推送给前端,简单,自动重连;适合问答打字机。
Websocket:双向全双工,前后端互相发消息;适合聊天双向实时交互,协议复杂。
项目 Agent 流式回答选用 SSE 足够。
19. Redis 的过期策略是什么?如果大量 session 过期,会发生什么
Redis 过期:惰性删除 + 定期删除。大量 key 同时过期会造成 CPU 抖动。
项目规避:session TTL 加随机偏移量,避免大量会话同时过期。
20. Milvus 索引类型,什么时候选什么索引
IVF_FLAT:小数据集,高精度;IVF_SQ8:压缩向量,节省内存,大数据量,精度轻微下降。
项目知识库使用 IVF_SQ8,平衡内存和检索速度。
21. 什么是 Agent 的 Tool 调用循环终止条件?
- 设置最大调用轮次(例如最多 3 轮 function call)
- 检测工具返回结果已经足够回答用户问题,不再继续调用工具
防止无限循环调用 tool。
22. FastAPI 的 worker 类型:uvicorn 的 uvloop 和 workers,同步 / 异步 worker 选型
uvicorn 默认用 uvloop,提升异步 IO 性能;异步代码用单个 worker+uvloop,多 worker 会多进程。
注意:多进程会复制内存,Embedding 模型加载到多进程会占用大量内存。项目中 Embedding 服务独立部署,不和 web 服务放一起。
23. 什么是 Starlette,和 FastAPI 关系
FastAPI 基于 Starlette(轻量异步 web 框架),Starlette 负责底层 http、中间件、StreamingResponse;FastAPI 在上面增加 Pydantic、路由、Depends 等能力。
24. 异步代码的异常捕获怎么处理?
使用 try/await,在异步函数内捕获异常;全局中间件捕获未处理异常,包装为统一返回。
25. 什么是 token,如何估算 prompt 的 token 数量,项目中怎么控制上下文上限
token 是文本的最小分片单元;用 tiktoken 库估算 token 数量;超过阈值做摘要压缩,截断旧对话。
import tiktoken
enc = tiktoken.encoding_for_model("gpt-3.5-turbo")
token_count = len(enc.encode(system_prompt + query))