10.14面试高级python和agent

1. Python GIL 是什么?项目中如何使用的

回答

GIL (Global Interpreter Lock),全局解释器锁

  • CPython 特有的锁,同一时刻一个 Python 进程内只能有一个线程执行 Python 字节码。
  • GIL 不会锁住 IO,IO 阻塞的时候 GIL 会释放;但是 CPU 密集型计算,多线程无法利用多核。
  • 两个结论:
    1. IO 密集型任务(网络请求:调用 LLM、Milvus、Redis、HTTP 接口):多线程 /asyncio 异步可以获得收益,等待网络的时候释放 GIL。
    2. CPU 密集(大量文本分片、Embedding 计算):多线程没用,要用多进程,绕过 GIL。

✅ 项目场景(你的 Agent 项目)

  1. FastAPI 里的接口请求,属于IO 密集:调用 LLM、Milvus、Redis、数据库,大量时间在等待网络 IO,所以我们用异步 async。IO 等待期间释放 GIL,并发处理更多请求。
  2. 文档流水线:文档分片、文本清洗、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中间件体系。

常见中间件类型:

  1. 内置:CORSMiddleware(跨域)
  2. 自定义中间件:全链路 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 项目

  1. 依赖函数get_tenant_context:从 request.state 读取 token 解析出来的 tenantId,校验租户状态,注入到 Agent 接口,所有 RAG / 工具调用路由复用。
  2. 数据库会话依赖:get_db,获取 PG 连接会话。
  3. 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 服务应该异步还是同步

回答

区分场景

  1. async def 异步接口 :适合 IO 密集任务。等待网络(LLM、Milvus、Redis、DB),await 释放事件循环,worker 可以处理其他请求。
    适用:Agent 对话接口、RAG 检索、调用 LLM、向量库查询。

✅ Agent 主链路推荐异步,Agent 核心链路全部是 IO 等待:LLM 调用、Milvus 检索、Redis 会话读取、PostgreSQL 查询,大量 IO 等待,异步可以提升并发。

  1. 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 大模型流式返回)。

回答

  1. 迭代器 (Iterator) :实现__iter__ + __next__,保存状态,逐个返回元素,手动迭代。
  2. 生成器 (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 常用装饰器:

  1. @app.get / @app.post / @app.put:注册路由,最基础
  2. @app.websocket:websocket 路由
  3. 自定义装饰器:用来做接口拦截、权限校验、日志统计(注意:异步接口要写异步装饰器)

补充: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 ,中文效果好,适合金融 / 知识库文档向量化。
    使用流程:
  1. 文档分片后,把 chunk 文本送入 Embedding 模型,输出 float 向量数组。
  2. 入库 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 区别,项目用过哪些,代码解释如何调用?

回答

概念区分

  1. Tool(工具 / Function Call)
    LLM 原生能力:LLM 识别意图,输出结构化函数调用参数,由程序执行函数,把结果丢回给 LLM 继续推理 。
    本质:LLM 驱动的函数调用,OpenAI function call / Anthropic tool call 都属于这一类。

粒度:单个可调用函数,比如查询工单、计算风险分。

  1. Skill(技能)
    一组 Tool + 执行逻辑 + 提示词 + 业务编排 ,封装成一个完整能力。
    Skill 是更高一层的业务抽象,一个 Skill 可以包含多个 Tool、多轮工具调用、前置校验、结果后处理。

举例:【工单风险评估 Skill】,内部会连续调用query_work_order、risk_score_calculate两个 tool,并且内置业务判断逻辑,这整套打包叫 Skill。

简单一句话区分:

  • Tool = 最小可调用函数单元
  • Skill = 业务能力包,由多个 Tool + 编排逻辑组成
  1. 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(最小函数单元)
    1. query_work_order:查询工单详情
    2. risk_score_calculate:风险打分
    3. 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 调用死循环怎么避免?

回答:

两层防护:

  1. 全局设置最大工具调用轮次,超过直接终止;
  2. 每次拿到工具返回结果,Prompt 引导 LLM 判断信息是否足够回答用户,不需要继续调用工具。

Q4:Tool 返回结果很大怎么办?

回答:

对 tool 返回结果做摘要、截断,只保留 LLM 推理需要的关键字段,减少 token 消耗,避免 prompt 超长。

Q5:Tool 调用存在并发 / 数据脏写问题怎么处理?

回答:

写类型 tool(update_task_status)增加幂等 key,防止 LLM 重复触发工具调用,重复更新状态。

10. Agent 幻觉如何减少,如何提高准确性,举例场景

回答

幻觉本质:LLM 在没有足够参考信息时编造内容。多层策略组合:

  1. RAG 层:分片优化(标题感知分片)、多路召回 + 重排,提升参考文档质量,把正确的上下文给到 LLM。
  2. Prompt 约束 :强制要求模型只能基于提供的参考文档回答;无相关信息直接回答不知道,禁止编造;输出附带引用 chunk_id 溯源。
  3. 结果校验层:对输出结果做校验,引用分片相似度低于阈值直接拒绝生成答案。
  4. 业务校验:金融质检场景,AI 输出仅作为辅助,人工复核。
  5. 拒绝编造策略:召回分数低于阈值,直接返回知识库无相关信息,不送入 LLM。

✅ 场景例子(金融质检)

当 Agent 判断录音是否违规,必须基于召回的监管文档片段;Prompt 强制要求:如果参考文档没有对应的监管条款,不能自行编造违规结论,同时输出引用的分片 ID 用于溯源。

代码示例(Prompt 约束模板)

复制代码
system_prompt = """
你只能基于【参考文档】回答用户问题。
如果参考文档没有对应信息,直接回复"没有找到相关依据",禁止编造任何规则。
输出结果附带引用的分片ID。
参考文档:{context}
"""

口述总结:幻觉是组合方案,RAG 提升召回质量,Prompt 强约束,低相似度召回直接拦截,输出强制溯源;金融场景 AI 结果还要人工复核。

11. Agent 多轮对话怎么设计

回答

整体思路:会话 ID 隔离,上下文分段存储,长短记忆分离

  1. 生成唯一 session_id,绑定租户 + 用户,作为会话唯一标识。
  2. 短期记忆(Redis):存储最近多轮对话(user、assistant、tool 调用记录),设置 TTL;每次请求读取历史,拼入 prompt。
  3. 上下文窗口管理:对话累积过长时,做摘要压缩,防止 token 超限。
  4. 长期记忆(PostgreSQL):会话全量记录异步持久化,用于审计,不会全部塞到 prompt。
  5. 会话隔离:不同 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 超时如何处理,如何优化

回答

处理策略(分层)

  1. 请求层超时设置:LLM http 客户端设置 timeout,区分连接超时、读取超时。
  2. 重试策略:网络抖动导致的短暂超时,做有限次数重试(指数退避;业务敏感场景不能无限重试,防止重复调用 LLM)。
  3. 熔断降级:滑动窗口统计 LLM 接口失败率,连续失败达到阈值,触发熔断,不再调用 LLM,直接返回降级结果。
  4. 链路超时控制:FastAPI、Nginx 设置最大超时,防止请求长时间挂住占用 worker。

优化手段

  1. 减少单次 prompt token 数量:上下文摘要、缩短参考分片。
  2. 开启 LLM 流式 SSE,提前返回 token,前端逐步渲染,用户感知等待变短。
  3. 缓存高频相同 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 调用循环终止条件?

  1. 设置最大调用轮次(例如最多 3 轮 function call)
  2. 检测工具返回结果已经足够回答用户问题,不再继续调用工具
    防止无限循环调用 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))
相关推荐
希洛兔2 小时前
原码、反码、补码:一张图讲透计算机里的负数表示
算法·面试
溪语流沙2 小时前
面试加分项!Manacher:O(n)求最长回文子串的对称之美
java·python·leetcode·面试·职场和发展
西柚小萌新11 小时前
【岗位面试】--成都博维数孪 - 工业 AI 解决方案顾问
人工智能·面试·职场和发展
小 黄 鸡11 小时前
Flutter 面试必会知识点详解(从基础到进阶)
flutter·面试·职场和发展
我命由我1234517 小时前
田野笔记极简理解
经验分享·笔记·学习·职场和发展·求职招聘·职场发展·学习方法
局部有解19 小时前
Leetcode Hot100 - 最长连续序列
算法·leetcode·职场和发展
java_nnnn20 小时前
LeetCode刷题滑动窗口(最大连续1的个数三+将x减到0的最小操作数)Java
算法·leetcode·职场和发展
SamDeepThinking20 小时前
synchronized 和 lock 有什么区别?
java·后端·面试