Skip to content

高频问答

这份的边界

只覆盖简历上写了的技术。简历没写的(Kafka、K8s、微调、图数据库)不在这里—— 不是它们不重要,是面试官通常不会拿你没写的东西为难你,把时间花在能被问到的地方。

每题三部分:答题要点是骨架,别照背;加分项是那一句让面试官眼前一亮的话;别踩的坑是会被反问的地方。

自测方法

先盖住答案自己说一遍,说不出来的在标题前打个 ,第二轮只看打星的。 所有涉及原理的题都链到了公开知识站的 guide 页,答不上来就去补,别在这里死记结论。

1. Python 语言与异步

对应简历:「编程语言:Python、TypeScript,具备异步编程、类型建模及结构化数据处理能力。」

这一节是你的主动进攻区

你是前端出身,事件循环这套东西你在 JS 里已经用了五年。 面试官问 asyncio 的时候主动做类比,效果比单纯答对好很多—— 它同时证明了两件事:你懂异步模型的本质,而且你的前端背景是资产不是包袱。

Q1 · asyncio 的事件循环和 JS 的事件循环有什么区别?

答题要点

  • 相同的部分:都是单线程 + 事件循环 + 非阻塞 IO,本质都是「遇到 IO 就让出控制权,别干等」。
  • 不同点一:JS 的异步是抢不走的自动调度,Promise 一旦创建就会被推进;Python 的协程必须被 await 或包成 Task 才会执行,光调用一个 async 函数什么都不会发生。
  • 不同点二:JS 有明确的宏任务/微任务两级队列;asyncio 是一个 ready 队列 + selector(epoll)+ 定时器堆,没有微任务这层概念。
  • 不同点三:JS 里几乎所有 IO 都是异步的;Python 生态里同步库占大多数,在协程里误调一个同步库就会把整个事件循环卡死,这是实际项目里最常见的事故。

加分项:「所以我在 FastAPI 项目里有个习惯:任何第三方库引入前先确认它有没有 async 版本,没有的就用 asyncio.to_thread 包一层,不让它进事件循环。」 别踩的坑:不要说「asyncio 就是 Python 版的 Promise」。两者一个是调度模型一个是值容器,说错这个会被立刻追问。 原理见 node-asyncpython-engineering

Q2 · async def 函数被调用时发生了什么?

答题要点

  • 调用一个 async def 函数不执行函数体,只返回一个协程对象。它要被 await、或者被 asyncio.create_task() 包成 Task,才会真正跑。
  • await 做两件事:把控制权交回事件循环,并且注册「等这个东西完成之后从这里继续」。协程的局部状态由 Python 的帧对象保存,所以恢复时能接着往下走。
  • 事件循环拿到控制权后,去 ready 队列里挑下一个可执行的任务;IO 完成由 selector 通知,定时器由最小堆管理。
  • 只有 await 才是让出点。一段纯 CPU 计算的协程,中间没有 await,就是不可打断的——事件循环期间干不了别的事。

加分项:「我在客服项目里踩过一次:一个协程里做了大批 JSON 解析和字符串处理,没有 await,结果 SSE 的心跳被推迟,前端以为断连了。后来把它挪到 to_thread 里。」 别踩的坑:不要说「await 会阻塞」。await 让出的是控制权,阻塞的是当前协程而不是线程,这两个说法差别很大。

Q3 · 协程、线程、进程怎么选?

答题要点

  • IO 密集 + 高并发用协程:没有线程切换和锁开销,一个进程里挂几千个等待中的请求很轻松。Agent 服务基本都属于这一类——大部分时间在等模型、等数据库、等外部接口。
  • 需要用同步库、又不想改造,用线程:线程能被操作系统抢占,所以同步阻塞调用不会拖死别人。代价是有 GIL,CPU 密集场景多线程没有加速。
  • CPU 密集用进程:绕开 GIL。比如批量文档解析、OCR 这类,我会放到独立的 worker 进程里。
  • 实际项目通常是混着用:FastAPI 主进程跑协程,阻塞库放线程池,重的解析任务交给独立的 ARQ worker 进程。
  • 协程里必须调同步阻塞库时,用 await asyncio.to_thread(fn, ...) 扔到线程池;CPU 密集的(文档解析、OCR)线程池救不了,要交给独立进程。数据库这类要优先换异步驱动(asyncpg、SQLAlchemy async),而不是把同步驱动包进线程。

加分项:「判断标准我一般是看『这段代码在等谁』。等网络就用协程,等 CPU 就换进程,等一个改不了的同步库就用线程。」 别踩的坑:不要说「协程比线程快」——协程的优势是并发规模和调度开销,单个任务并不会执行得更快。另外别在协程里用 time.sleep()(该用 asyncio.sleep)或 requests(该用 httpx.AsyncClient),这两个是面试官最爱举的反例。

Q4 · GIL 是什么?对你的服务有什么实际影响?

答题要点

  • GIL 是 CPython 解释器里的一把全局锁:同一时刻只有一个线程在执行 Python 字节码。它保护的是解释器内部状态(比如引用计数),不是你的业务数据。
  • 影响面:多线程做纯 Python 的 CPU 计算拿不到多核收益;但IO 等待时 GIL 会被释放,所以多线程做 IO 依然有效。
  • C 扩展在计算期间可以主动释放 GIL,所以 NumPy 这类库的多线程是能吃到多核的。
  • 绕开方式:多进程、把计算下沉到 C 扩展、或者把重活交给独立服务。CPython 3.13 有实验性的 free-threaded 构建,但生产上我不会依赖它。

加分项:「GIL 对我们这类 Agent 服务的影响其实很小,因为瓶颈几乎全在等模型和等数据库。真正需要注意的是别在协程里写 CPU 密集代码——那个问题跟 GIL 无关,是事件循环被占住了。」 别踩的坑:不要说「有 GIL 所以 Python 多线程没用」。IO 场景多线程完全有用,这个说法过于绝对。

Q5 · 要批量请求几百个 Embedding,怎么控制并发?

答题要点

  • 并发上限用 asyncio.Semaphore 控住,通常包成一个 async def limited(x): async with sem: ... 的小封装,再 gather。
  • HTTP 客户端要复用同一个 httpx.AsyncClient,靠 httpx.Limitsmax_connectionsmax_keepalive_connections;每次请求新建 client 会丢掉连接池。
  • 超时必须分开设:连接、读、写、从池里拿连接各有各的超时;只设一个总超时排查问题会很难。
  • 失败要能重试:区分可重试(429、5xx、超时)和不可重试(400、鉴权失败),指数退避 + 随机抖动,别所有请求同时重试。
  • 能批处理就批处理:Embedding 接口通常支持一次传多条文本,先攒 batch 再控并发,比单条高并发更省也更稳。

加分项:「并发数不是越大越好,上游有限流。我一般是先压出上游的实际吞吐上限,再把 Semaphore 设在它下面一点,留出重试的余量。」 别踩的坑asyncio.gather 默认一个任务抛异常会把异常抛出来,其他任务还在后台跑。要么用 return_exceptions=True 自己收集结果,要么用 3.11 的 TaskGroup(它会取消同组其他任务)。这两种语义完全不同,说清楚哪种是你要的。

Q6 · Pydantic v2 你用来做什么?和 v1 有什么区别?

答题要点

  • 我用它做三件事:API 的入参出参校验LLM 结构化输出的落地校验工具调用参数的 schema 定义。第二和第三件是 Agent 项目里的关键——模型返回的 JSON 必须先过一遍校验才能用。
  • v2 的校验核心用 Rust 重写(pydantic-core),性能相比 v1 有量级提升;API 也改了:model_validate / model_dump 取代 parse_obj / .dict()field_validator / model_validator 取代 validator / root_validator
  • model_json_schema() 能直接导出 JSON Schema,正好喂给 Tool Calling 的函数定义——工具的参数定义和运行时校验用的是同一份模型,不会不一致。
  • TypeAdapter 可以给非模型类型(比如 list[dict])做校验;Field 上的约束(ge、max_length、pattern)会一起进 schema,模型能看到这些约束。
  • ORM 对接用 model_config = ConfigDict(from_attributes=True)

加分项:「工具定义我是从 Pydantic 模型生成 JSON Schema 的,所以模型看到的参数约束和我执行前校验的约束永远是同一套。手写两份 schema 迟早会漂。」 别踩的坑:别把 Pydantic 说成「就是个数据类」。它的价值在校验和序列化边界,尤其是给不可信输入(用户请求、模型输出)设一道确定性的关

Q7 · 类型注解在你项目里有什么实际价值?

答题要点

  • LangGraph 的 State 就是用 TypedDict 或 Pydantic 模型定义的,字段和 reducer 写清楚,才知道每个节点能读什么、能改什么。State 没有类型,多人协作会很快失控。
  • 结构化输出的落点:模型返回什么结构,用类型描述出来,配合校验做重试——校验失败就把错误信息喂回模型让它改。
  • 配合 mypy 或 pyright 做静态检查,异步代码里尤其有用,能提前抓到「忘了 await」这类问题(返回了协程对象而不是值)。
  • 前端出身这块我有惯性优势:TypeScript 的类型建模思路直接迁移过来,Union、字面量类型、判别式联合在 Python 里也都有对应。

加分项:「Tool 的参数用判别式联合(discriminated union)建模特别好用——不同工具的参数结构不一样,用一个字段做判别,校验时就能自动路由到对应的模型。」 别踩的坑:不要说「Python 的类型注解运行时会校验」。注解本身不校验,是 Pydantic / FastAPI 在运行时读注解做校验的,这个区别面试官会抠。

2. Agent 与 LangGraph

对应简历:「Agent 开发:LangGraph、LangChain、ReAct、Multi-Agent、Tool Calling、MCP、Checkpoint、Human-in-the-Loop。」

Q1 · 为什么用 LangGraph 而不是 LangChain 的 Chain?

答题要点

  • Chain 是线性的、一次性的:输入进去,输出出来,中间状态不可见。想加分支、加循环、加重试,就得在业务代码里手写调度。
  • LangGraph 是显式的状态图:节点是函数,边是转移,条件边做路由,状态在 State 里累积。天然支持循环(ReAct)、分支(意图路由)和多次重试。
  • 我真正看重的是三点:中间状态可见(每一步的产物都在 State 里,排查问题能看到全过程)、可中断可恢复(配合 Checkpoint 能做 HITL)、结构和业务解耦(改路由不用动节点实现)。
  • BI 项目那条链路是最好的例子:SQL 校验失败要回到生成节点重试,最多几次,超了就走降级——这个用 Chain 表达非常别扭,用图就是一条条件边。

加分项:「判断标准我一般是:如果流程里有『可能要回头』的地方,就用图;纯前向的单次调用,Chain 甚至裸调 SDK 都够了,没必要为了用框架而用框架。」 别踩的坑:不要贬低 LangChain。它的模型抽象、文档加载器、检索器封装在项目早期很省事。说「LangGraph 更适合有状态和分支的编排」,不要说「LangChain 不行」。 原理见 agent-langgraphagent-intro

Q2 · LangGraph 的 State 怎么设计?多个节点同时写一个字段怎么办?

答题要点

  • 节点返回的是状态的部分更新,不是整个 State;框架把返回值合并进全局状态。所以节点之间不需要知道彼此。
  • 合并规则由 reducer 决定:默认是覆盖,需要累积的字段要显式声明,比如 Annotated[list[Message], add_messages] 把消息追加而不是替换。
  • 设计原则我一般是三条:字段要少(只放跨节点需要的)、语义要清(谁写谁读心里有数)、大对象不进 State(原始文档内容放存储,State 里只放引用和 ID)。
  • 并行节点写同一个字段必须有 reducer,否则行为不可控;这也是为什么并行分支通常各写自己的字段,最后由一个汇聚节点合并。

加分项:「我给 State 定过一个规矩:能重算的不存,能溯源的只存 ID。检索到的原文我只存 chunk ID 和位置,需要的时候再取,这样 Checkpoint 落库的体积能小一个量级。」 别踩的坑:不要把 State 当全局变量用。什么都往里塞,最后 token 成本和调试成本都会爆——这个问题我在客服项目里真踩过(见 10-口述稿 §3 范例一)。

Q3 · Checkpoint 是干什么的?你用它做了什么?

答题要点

  • Checkpoint 把每一步执行后的状态持久化,按 thread_id 组织成一条会话的执行历史。作用有三个:恢复中断后继续多轮对话的上下文延续
  • 开发时用内存实现就够;生产要用持久化后端(PostgreSQL / SQLite 的 checkpointer),否则服务重启状态全丢。
  • 我最主要的用途是 HITL:走到敏感节点前中断,把待确认的动作写进 Checkpoint,人确认后从那个点恢复继续执行,中间进程重启也不影响。
  • 附带好处是可观测性:一次执行的每一步状态都留在库里,线上出问题可以按 thread_id 把整条轨迹翻出来复现。
  • 要注意清理策略:Checkpoint 会持续增长,需要按会话结束或时间做归档删除。

加分项:「Checkpoint 让『人一小时后才来点确认』变成一个正常场景而不是异常场景——会话状态不在内存里,所以确认可以隔很久,也可以换一个进程处理。」 别踩的坑:不要把 Checkpoint 和「会话记忆」混为一谈。前者是执行状态的快照(用来恢复),后者是对话内容的存储(用来给模型上下文)。我的项目里会话记忆在 Redis,Checkpoint 在 PostgreSQL。

Q4 · 什么时候该拆子图?子图和父图怎么隔离状态?

答题要点

  • 拆的判断标准:这一段有自己独立的循环、自己的工具集、自己的失败处理,而且父图不关心它的中间过程。政务项目的三类场景、客服项目的五个领域,都符合这个特征。
  • 隔离机制其实很朴素:父图和子图通过重叠的 State key 交换数据,不重叠的 key 就是子图私有的。所以做隔离就是设计字段边界,不需要额外机制;如果两边的 State schema 不同,就在调用处做输入输出的转换。
  • 好处:父图上下文小、子图可以单独测试、改一个领域不影响其他领域。
  • 代价:调试要逐层看,跨子图的信息要显式传递。

加分项:「我不按工具数量拆,按业务域拆。判断标准是『这组工具会不会被同一个意图同时用到』——会的放一起,不然跨子图来回传参比合在一起更麻烦。」 别踩的坑:不要为了「架构好看」拆子图。子图数量上去之后,路由错误的排查成本是实打实增加的。 原理见 agent-multi-agent

Q5 · 循环怎么控制?怎么防止 Agent 死循环?

答题要点

  • 框架层有递归上限(recursion_limit),到了会抛异常——这是最后一道保险,不是正常控制手段。
  • 业务层要自己数:每类循环都要有显式的计数字段在 State 里,比如 SQL 重试次数、工具调用步数、澄清轮数,超了就走降级分支而不是抛错。
  • 检测重复动作:同一个工具、同样的参数连续调用两次以上,基本可以判定它卡住了,直接终止。
  • 还要有全局预算:总步数上限、总 token 预算、总耗时上限,三者任一超限就停。
  • 终止之后必须给用户一个可解释的结果,不能只返回一句「失败了」。我一般返回已经拿到的部分信息 + 转人工入口。

加分项:「我把『到达上限』当成一个正常分支来设计,不是异常。异常处理容易被漏,正常分支会被测试覆盖。」 别踩的坑:只靠 recursion_limit 兜底会有两个问题:一是它抛异常,用户拿到的是 500;二是它不区分是哪个循环失控的,排查时看不出原因。

Q6 · ReAct 的原理是什么?和直接让模型输出答案有什么区别?

答题要点

  • ReAct = Reasoning + Acting:模型交替产出「想法」和「动作」,动作的执行结果(Observation)再回到上下文,形成 Thought → Action → Observation 的循环,直到它认为可以给答案。
  • 关键价值是把外部世界的真实反馈接进推理过程。纯生成只能靠参数里的知识,ReAct 能查数据库、调接口,答案有事实依据。
  • 现在的工程实现基本不靠 Prompt 里的 "Thought:" 文本解析了,而是用原生的 Tool Calling:模型直接返回结构化的工具调用,稳定得多。
  • 代价是延迟和成本:每一轮都是一次模型调用,一个任务三四轮很常见。所以能一次问清的场景不该用 ReAct。

加分项:「我在客服项目里的实践是:只读查询类走 ReAct 让它自己决定调什么;写入类不让它自由决策,走固定流程加人工确认。自由度和风险要匹配。别踩的坑:不要把 ReAct 说成「一种 Prompt 模板」。它是一种控制循环,模板只是早期的实现方式。

Q7 · Tool Calling 的协议是怎么走的?参数不合法怎么处理?

答题要点

  • 流程是:我把工具的 JSON Schema 一起发给模型 → 模型返回 tool_calls(带 id、函数名、参数字符串)→ 我本地执行 → 把结果作为 tool 角色的消息带着对应的 tool_call_id 回传 → 模型继续。
  • 有几个工程细节必须处理:参数是字符串形式的 JSON,要先解析再校验;模型可能一次返回多个工具调用(并行);每一个 tool_call 都必须有对应的结果消息回传,少一个有些模型会直接报错。
  • 参数校验用 Pydantic,schema 和校验共用一份模型。校验失败不抛给用户,而是把结构化的错误信息作为工具结果回传,让模型自己改一次——这比直接失败的体验好很多,但要限次数。
  • 工具描述的写法直接影响调用准确率:说清什么时候用、什么时候不要用、参数的单位和格式。工具选错,一大半是描述写得不清楚。

加分项:「我会给工具描述里加反例:『查历史订单不要用这个工具』。加了反例之后误选明显减少——这一条比调模型参数有效得多。」 别踩的坑:不要说「模型会自动调用工具」。模型只是返回一个调用请求,执行是你的代码做的,权限、校验、幂等全在你这边。这个边界说清楚,是很关键的加分点。 原理见 agent-tool-calling

Q8 · MCP 是什么?解决什么问题?

答题要点

  • MCP(Model Context Protocol)是一个开放协议,用来标准化「模型/Agent 怎么接外部能力」。基于 JSON-RPC 2.0,传输可以是 stdio(本地进程)或 Streamable HTTP(远程)。
  • 核心原语三个:Tools(可调用的动作)、Resources(可读取的数据)、Prompts(可复用的提示模板)。
  • 解决的是 N×M 的集成问题:原来每个 Agent 都要为每个系统写一遍适配,有了协议之后,Server 实现一次,任何 MCP Client 都能用。
  • 和普通 Tool Calling 的关系:Tool Calling 是模型和你的代码之间的约定,MCP 是你的代码和外部能力提供方之间的约定。两者是上下游,不是替代关系。
  • 安全上要特别注意:接第三方 MCP Server 等于把外部内容引进上下文,要做工具白名单、参数校验,敏感操作仍然要人工确认。

加分项:「我自己搭过 Server 和 Client 跑通过。落地时我最关心的是权限——MCP 让接能力变容易了,但『容易接』和『可以随便调』是两件事,工具分级和确认机制不能因为换了协议就省掉。」 别踩的坑:不要把 MCP 说成「一种新的 Function Calling」。它是集成层协议,不改变模型侧的调用方式。

Q9 · HITL 怎么实现?超时没人确认怎么办?

答题要点

  • 实现靠中断 + Checkpoint:在敏感节点前中断执行,把「准备做什么、参数是什么」写进持久化状态;人确认后带着恢复指令从断点继续。
  • 前端展示要给足信息:动作名、参数、影响范围、可撤销性。让人能判断,而不是只给一个「确认」按钮。
  • 超时策略要明确:设一个业务能接受的时限,超时就关闭这一轮、记录原因、通知用户重新发起,绝不能默认放行。
  • 恢复时要重新校验一遍前置条件——人可能一小时后才点确认,订单状态可能已经变了。
  • 写入操作要带幂等键,防止确认之后的网络抖动导致重复提交。

加分项:「HITL 最容易被忽略的是恢复时的二次校验。人点确认那一刻的世界,和 Agent 决定要做这件事那一刻的世界,可能已经不一样了。」 别踩的坑:不要把 HITL 说成「加一个确认弹窗」。它的难点在有状态的暂停恢复和超时处理,弹窗只是最表层的一环。

3. RAG 与检索

对应简历:「RAG 检索:文档解析、语义分块、Embedding、BM25、向量混合检索、RRF、Rerank、Query Rewrite、引用溯源。」

Q1 · 文档怎么切分?切多大合适?

答题要点

  • 我的顺序是:先解析出结构,再按结构切。拿到 PDF/DOCX 先还原标题层级、段落、表格,然后按标题和语义边界切,而不是一上来就按字符数切。
  • 每个 chunk 要带元数据:文档 ID、页码、章节路径、版本。这些既是引用溯源的依据,也是过滤条件。
  • 大小是权衡:太小丢上下文(一个条款被切两半),太大稀释语义(向量表达不准,还挤占 Prompt)。我一般定几百 token 的量级,加重叠保证边界信息不丢。
  • 特殊结构要单独处理:表格不能按字符切(切了就读不懂),我会把表格整块保留并补一句表头说明;跨页的表格要先合并。
  • 政务这类有条款结构的,按条款切最自然,条款号本身就是最好的引用锚点。

加分项:「切分策略是我在 RAG 上收益最大的一次改动。从固定长度改成按标题层级 + 保留章节路径之后,召回质量的提升比我换 Embedding 模型明显得多。RAG 的效果问题,一半以上根子在解析和切分。别踩的坑:不要只答一个字数。面试官问切分,想听的是你怎么处理结构、表格、重叠和元数据。只说「512 加 50 重叠」会被认为只跑过教程。 原理见 rag-pipeline

Q2 · Embedding 模型怎么选?

答题要点

  • 先看硬约束:能不能出网。政务项目必须内网,所以只能选可本地部署的开源模型;商业项目可以考虑 API。
  • 再看语言和领域:中文场景要选中文效果好的(BGE、GTE 这一类),不要看英文榜单的排名。
  • 再看工程参数:维度(影响存储和索引大小)、最大输入长度(要能容纳你的 chunk)、推理速度(入库时的吞吐直接受它影响)。
  • 最后是必须在自己的评测集上验。公开榜单和你的语料分布不一样,我遇到过榜单更高的模型在我们数据上更差。
  • 换模型的代价要提前想清楚:换了就要全量重新向量化,几万个分块重跑一遍是有成本的,而且新旧向量不能混用。

加分项:「查询和文档要用同一个模型、同一种归一化方式,这个听起来是常识,但我见过入库和查询用了不同版本的模型,结果召回质量莫名其妙地差。」 别踩的坑:不要说「用最强的那个就行」。维度高一倍、延迟高三倍换来一个百分点,在入库量大的场景里不一定划算。

Q3 · pgvector 的索引怎么选?HNSW 和 IVFFlat 的区别?

答题要点

  • 两种都是近似索引,用来避免全表扫描;也就是说它们都会牺牲一点召回率换速度,这一点必须说出来。
  • IVFFlat:先聚类分桶,查询时只搜若干个最近的桶。需要先有数据才能建索引(要训练聚类中心),参数是 lists 和查询时的 probes。建索引快,占空间小,召回略差。
  • HNSW:分层的近邻图,参数是 mef_construction,查询时 ef_search 控制精度。不需要预先有数据,召回和延迟都更好,代价是建索引慢、内存占用高。
  • 距离算子要和建索引时声明的一致:<-> 是 L2,<=> 是余弦,<#> 是内积。索引和查询算子不匹配,索引就用不上。
  • 带过滤条件的查询是这里最大的坑:近似索引先按向量找,过滤可能把结果几乎全滤掉,导致返回数量不足。应对方式是提高候选量、用分区表或部分索引、或者用新版本的迭代扫描能力。

加分项:「我们默认选 HNSW,因为知识库是增量入库的——IVFFlat 需要有数据才能建索引,这个前提在持续写入的场景里很别扭。」 别踩的坑:不要说「加了索引召回率不变」。近似索引一定有召回损失,说不清这一点会被认为没实测过。

Q4 · 为什么要 BM25 加向量的混合检索?RRF 怎么融合?

答题要点

  • 因为两者的失效场景正好互补:向量擅长语义相似但对精确的编号、专有名词、罕见词很弱;BM25 擅长关键词精确匹配但完全不懂同义表达
  • 政务场景是最典型的例子:用户既会问「换身份证要带什么」(口语,靠向量),也会搜「浙政办发某某号」(文号,靠 BM25)。两类都是高频,所以必须双路。
  • RRF 的做法:对每一路的结果只取排名,每个文档的融合分是各路 1/(k + rank) 之和,k 通常取 60,然后按融合分重排。
  • RRF 的关键优势是不需要归一化分数。BM25 分数和余弦相似度量纲完全不同,做归一化要调参且换语料就失效;用排名就绕开了这个问题。需要偏向某一路时,给这一路加权重。
  • 融合之后一般还要过 Rerank 精排,融合只是把候选集合选好。

加分项:「我会用一句话说清 RRF 为什么合适:它只关心谁排前面,不关心分数有多高,所以两个完全不可比的打分系统也能融。」 别踩的坑:不要说「混合检索一定比单路好」。它增加了一套系统、增加了延迟,而且要维护索引同步。如果你的查询里没有精确匹配类的需求,单路向量可能就够。 原理见 rag-retrieval

Q5 · Rerank 是干什么的?为什么向量检索之后还要它?

答题要点

  • 向量检索是双塔结构:查询和文档各自编码成向量再算距离,文档向量可以预先算好,所以快,但查询和文档之间没有交互。
  • Reranker 是 cross-encoder:把查询和文档拼在一起过一遍模型,直接输出相关性分数。能捕捉细粒度的语义关系,明显更准。
  • 代价是不能预计算——每个候选都要跑一次模型,所以只能用在少量候选上。标准做法是「粗排召回几十条 → 精排取前几条」。
  • 精排还有个附带价值:它能把「相关但不是最相关」的往后压,直接改善进 Prompt 的证据质量,答案的稳定性跟着提升。
  • 候选数量要权衡:给 Reranker 50 条和 20 条,延迟差别明显,收益递减。这个数要在自己的评测集上试。

加分项:「Rerank 的收益在混合检索之后特别明显:RRF 融合出来的列表是两个不同尺度的结果拼的,用一个统一的模型重打一遍分,顺序才真正可信。」 别踩的坑:不要把 Rerank 和 Embedding 混为一谈。一个双塔一个交互式,一个能预计算一个不能——这是它们所有性能差异的根源。

Q6 · Query Rewrite 什么时候需要?怎么做?

答题要点

  • 最常见的场景是多轮对话里的指代消解。用户先问「社保怎么转」,接着问「那需要什么材料」——第二句直接拿去检索什么也搜不到,必须先补全成完整问题。
  • 第二种是扩展召回:一个问题改写成几个不同角度的查询并行检索,再合并结果(Multi-Query)。适合表述模糊、召回不稳的场景。
  • 第三种是术语对齐:用户说的是口语,文档里是规范表述,改写成文档的用词能提高命中率。BI 项目里就是把「卖了多少钱」对到「销售额」这个指标名。
  • 改写要按查询类型分流,不要无脑全改:明确指向某个文件或编号的查询,改写反而会破坏精确匹配。我是按查询类型选策略的。
  • 代价是多一次模型调用和延迟,而且改写本身会引入错误,所以我会保留原始查询也走一路。

加分项:「我保留原始 query 作为其中一路,融合之后再排序。这样改写失败的时候有兜底,不会因为改写把本来能召回的东西改跑了。」 别踩的坑:不要说「Query Rewrite 一定提升效果」。改写在精确检索类查询上是负收益,这个反例能证明你真的按场景做过分流。

Q7 · 引用溯源具体怎么做?怎么保证引用是真的?

答题要点

  • 前提是元数据在切分时就要留好:chunk ID、文档 ID、页码、章节或条款位置。溯源能力是入库阶段决定的,生成阶段补不回来。
  • 生成时把候选证据带 ID 一起给模型,要求输出里标注每段结论用了哪个 chunk;用结构化输出约束格式,不要让它自由写「参考资料如下」。
  • 不能只信模型说它引用了什么,要做校验:检查引用的 ID 是否在本次召回集合里、引用的原文片段能不能在该 chunk 里定位到。对不上就当引用无效。
  • 前端要能点开定位到原文的位置,这是「引用可信」在体验上的落点——用户能自己验证。
  • 引用不足或者证据不支撑就拒答。企业和政务场景里,一次自信的错答比十次「没找到」代价大得多

加分项:「我把引用校验做成了确定性的一步,不依赖模型自觉。模型说它引了 chunk 7,我就去看 chunk 7 在不在这次召回里、内容对不对得上——校验必须是代码做的。」 别踩的坑:不要说「让模型输出引用就行」。模型会编 ID、会引用没召回到的东西,没有校验环节这个功能就是装饰。 原理见 rag-citation

Q8 · 什么时候该拒答?怎么判断证据不足?

答题要点

  • 判断信号有几个,我一般组合用:召回结果为空;最高相似度或 Rerank 分数低于阈值;召回的内容和问题明显不在一个主题上;模型自己表示无法从证据得出结论。
  • 阈值不能凭感觉定,要在评测集上找那个「拒答率上升但错答率明显下降」的点。这是一个业务取舍,不是技术最优解。
  • 拒答的输出形式很重要:不能只说「我不知道」。要给出可能相关的文件让用户自己判断,给转人工入口,说明为什么答不了。
  • 不同场景的阈值应该不一样:政务和金融要保守,内部知识库可以宽松一点。
  • 拒答的 case 要留下来看——它们是最好的评测集来源,说明这些问题现有知识覆盖不到。

加分项:「拒答率我是当成一个正常指标在看的,不是越低越好。拒答率为零通常意味着它在硬答,那才是问题。」 别踩的坑:不要说「用模型判断自己有没有把握」。模型的自我评估不可靠,该用检索分数、证据覆盖这些可测量的信号,模型判断只能作为辅助。

4. FastAPI 与后端工程

对应简历:「后端框架:FastAPI、NestJS、Pydantic、SQLAlchemy、异步任务、SSE、WebSocket。」

Q1 · FastAPI 的依赖注入你怎么用?

答题要点

  • 三个典型用途:数据库会话(用 yield 依赖,请求结束自动关闭)、当前用户和权限(解析 JWT 并校验角色)、配置和客户端(复用 httpx client、Redis 连接池)。
  • 依赖在同一个请求内会被缓存:同一个依赖被多处声明,只执行一次。想每次都执行要显式关掉缓存。
  • 依赖可以嵌套:get_current_user 依赖 get_db,权限依赖再依赖 get_current_user,形成一条声明式的校验链。
  • 测试时用 dependency_overrides 把真实依赖换成假的,这是 FastAPI 测试最方便的一点。
  • 我会把权限检查做成依赖,这样每个路由的权限要求在签名里就能看出来,不用翻函数体。

加分项:「但权限我不会只做在依赖里。知识库这种资源级的权限必须下推到查询条件,依赖只挡住入口,挡不住『有权限访问 A 库的人查到了 B 库的内容』。」 别踩的坑:不要在依赖里做耗时的阻塞操作。它在每个请求路径上,一慢就是全局慢。 原理见 fastapi-advanced

Q2 · 中间件和依赖有什么区别?分别用来做什么?

答题要点

  • 中间件包在整个请求响应周期外面,能拿到 request 和 response,适合横切关注点:日志、traceId、耗时统计、CORS、全局异常兜底。
  • 依赖在路由级别,能拿到解析后的参数和类型,适合业务前置条件:认证、鉴权、取会话、参数补全。
  • 区别的核心:中间件不关心具体路由,依赖是路由声明的一部分。需要按路由差异化的逻辑放依赖,全局统一的放中间件。
  • 流式响应要特别小心中间件:基于 BaseHTTPMiddleware 的实现可能会把响应体缓冲起来,直接破坏 SSE 的实时性。这种场景我用纯 ASGI 中间件。
  • 顺序有讲究:异常处理要能覆盖到日志中间件,注册顺序反了就抓不到。

加分项:「SSE 项目里我踩过中间件缓冲的坑——加了一个统计响应体大小的中间件,结果流式变成了一次性返回。任何中间件上线前都要拿流式接口验一遍。别踩的坑:不要说「中间件和依赖差不多」。这题问的就是边界,答不清会被认为没做过复杂后端。

Q3 · SSE 和 WebSocket 怎么选?

答题要点

  • SSE 是单向的(服务端推客户端)、基于普通 HTTP、文本协议,浏览器原生支持自动重连和 Last-Event-ID 续传。
  • WebSocket 是双向的、需要协议升级、支持二进制,但重连、心跳、鉴权都要自己实现。
  • Agent 的流式输出是典型的单向场景:用户发一次请求,服务端持续推 token 和阶段事件。SSE 更简单、更贴合 HTTP 生态(网关、鉴权、日志都能直接用),所以我选 SSE。
  • 需要双向高频交互(协同编辑、语音、实时控制)才用 WebSocket。我在掌奇做企业应用时用过 WebSocket 做实时推送,那个场景确实需要双向。
  • SSE 的工程坑:反向代理要关缓冲(nginx 的 X-Accel-Buffering: no 或关 proxy_buffering)、要定期发心跳防中间层超时断连、浏览器原生 EventSource 不能自定义请求头(所以带 token 的场景我用 fetch + ReadableStream)。

加分项:「我在设计事件协议时会把事件类型分清:token 增量、阶段状态、工具调用、引用、错误、结束各是一类。前端能按类型分别渲染,而不是把所有东西塞在一个 message 里自己解析。 这个协议是我定的,前端也是我写的,所以两边都比较克制。」 别踩的坑:不要说「SSE 就是长轮询」。长轮询是反复建连,SSE 是一条连接持续推送。 原理见 agent-streaming

Q4 · SQLAlchemy 2.x 的异步用法有什么坑?

答题要点

  • 引擎和会话要用异步版本:create_async_engine + async_sessionmaker,会话通过 FastAPI 依赖按请求创建和关闭。
  • 最经典的坑是懒加载:异步会话里访问未加载的关系属性会报 MissingGreenlet,因为懒加载会触发同步 IO。解决办法是查询时显式 selectinload / joinedload,或者用 AsyncAttrs 的 await 方式访问。
  • expire_on_commit=False 一般要设上,否则 commit 之后访问对象属性会触发重新加载,在异步下同样会出问题。
  • 查询用 2.0 风格的 select(),配合 await session.execute();旧的 Query API 在异步里不要用。
  • 会话不是线程/任务安全的,不要把一个 session 在多个并发任务之间共享

加分项:「我的习惯是在仓储层就把需要的关系一次查出来,不让 ORM 对象逃到业务层之后再触发加载。这样既避免 MissingGreenlet,也顺手把 N+1 问题堵住了。」 别踩的坑:不要说「加了 async 就是异步的」。驱动必须是异步驱动(asyncpg/aiomysql),用同步驱动包一层还是会阻塞事件循环。

Q5 · Alembic 迁移怎么管?autogenerate 靠得住吗?

答题要点

  • 流程是:改模型 → alembic revision --autogenerate人工检查生成的脚本 → 应用。第三步不能省。
  • autogenerate 有明确的盲区:识别不出重命名(会生成删列加列,数据就丢了)、对服务端默认值和某些类型变更不敏感、自定义类型(比如 pgvector 的 vector)需要额外配置才能正确渲染。
  • 数据迁移和结构迁移要分开。加一个非空列的正确做法是三步:先加可空列 → 回填数据 → 再改非空,一步到位会锁表或直接失败。
  • 每个迁移都要能回滚,downgrade 不能空着;上线前在准生产库上跑一遍。
  • 分支合并容易出现多个 head,要及时 merge,否则后面的人根本没法迁移。

加分项:「pgvector 的向量列我特别检查过 autogenerate 的输出——自定义类型是最容易被生成成错误 DDL 的地方,这个坑不看一眼就会带到线上。」 别踩的坑:不要说「autogenerate 生成完直接跑」。这句话会让面试官觉得你没上过生产。

Q6 · JWT + RBAC 你是怎么设计的?

答题要点

  • JWT 里只放身份和少量稳定信息(user id、角色),不放会变的权限明细——权限变了 token 里的还是旧的。
  • access token 短期 + refresh token 长期。JWT 本身无状态、不可撤销,需要强制下线就得配一个吊销名单(我用 Redis 存 jti 黑名单,TTL 设成 token 的剩余有效期)。
  • RBAC 分两层:角色级决定能进哪些功能(用 FastAPI 依赖挡在路由上);资源级决定能看哪些数据(知识库级的 ACL)。
  • 资源级权限必须下推到查询条件。知识库项目里,用户可见的知识库 ID 集合是向量检索的过滤条件之一,不是查完再过滤——查完再过滤既漏数据又浪费 top-k 名额。
  • 敏感操作除了权限,还要有审计:谁、什么时候、对哪个资源做了什么,全部落日志。

加分项:「我的原则是权限至少挡两层:入口挡一次(快速失败、给明确错误),数据层再挡一次(防止任何路径绕过)。企业知识库里跨库召回是最严重的事故类型,不能只靠一层。」 别踩的坑:不要把 JWT 说成「比 session 更安全」。它是无状态更易扩展,但撤销困难、载荷可见(只是签名不是加密),这些代价要说出来。

5. 数据库与存储

对应简历:「数据与部署:PostgreSQL / pgvector、MySQL、Redis、Qdrant、Elasticsearch、Docker、Linux。」

Q1 · 为什么知识库项目用 PostgreSQL,BI 项目用 MySQL?

答题要点

  • 知识库选 PG 的直接原因是 pgvector:向量和业务数据放同一个库,权限、文档状态这些过滤条件能和向量检索写在同一条 SQL 里,不用维护两套数据的一致性。
  • PG 的其他加分点:jsonb 和数组类型适合存元数据、支持部分索引和表达式索引、事务型 DDL(迁移失败能回滚)、扩展生态丰富。
  • BI 用 MySQL 是因为业务数据本来就在 MySQL——数据源不是我选的,Text2SQL 要适配现有的数仓,这一点要老实说。
  • 两者的机制差异我会提一句:MySQL InnoDB 是聚簇索引(数据按主键组织,二级索引回表),PG 是堆表加独立索引;默认隔离级别 MySQL 是 RR,PG 是 RC。
  • 结论是:选型很多时候是被约束决定的,不是纯技术偏好。

加分项:「向量库要不要独立部署,我的判断标准是『过滤条件在哪』。过滤条件和业务数据强相关,就优先 pgvector;如果检索是独立的、量级又大,才值得引入专门的向量库。」 别踩的坑:不要说「PG 比 MySQL 好」。面试官如果用的是 MySQL,这句话就是硬碰。讲清适用场景就够了。 原理见 mysql-table-design

Q2 · pgvector 和 Qdrant 怎么选?你两个都用过,区别在哪?

答题要点

  • pgvector 的优势是一体化:和业务数据同库同事务,删文档和删向量能在一个事务里完成;过滤、JOIN、权限都用 SQL 表达;运维只多一个扩展。
  • Qdrant 的优势是专业化:payload 过滤和 ANN 检索是协同设计的(带过滤时召回质量更可控)、支持量化压缩内存、原生分片和副本、高 QPS 下表现更好。
  • 我的实际选择:知识库项目用 pgvector(量级可控、过滤条件多、想少一个系统);BI 项目用 Qdrant 存元数据向量(Schema Linking 的检索是独立的一块,和业务库没有事务关系)。
  • 引入独立向量库的真实代价是一致性:文档删了向量没删,这类脏数据排查很痛,需要额外的对账或双写补偿机制。
  • 判断标准:数据量、是否需要复杂过滤、是否需要和业务数据同事务、团队运维能力。

加分项:「我会先问一句『这批向量需要和业务数据保持事务一致吗』。需要就优先 pgvector——一致性问题的排查成本,比多维护一个系统的成本高得多。别踩的坑:不要说「专业向量库肯定更好」。在中小规模、过滤复杂的场景下,pgvector 的工程总成本往往更低。

Q3 · 索引怎么建?慢查询怎么排查?

答题要点

  • 排查一律从 EXPLAIN ANALYZE 开始:看它走没走索引、估行数和实际行数差多少、有没有全表扫描或临时排序。先看执行计划,再动手改。
  • 复合索引遵循最左前缀:(a, b, c) 能支撑 a、a+b、a+b+c 的查询,单独查 b 用不上。区分度高的列一般放前面,但要结合实际查询模式。
  • 常见失效原因:在索引列上套函数或做类型转换、以 % 开头的模糊匹配、隐式类型转换(字符串列传数字)。
  • 覆盖索引能避免回表;PG 里还有部分索引(只索引满足条件的行)和表达式索引,对「只查有效数据」这种场景很有用。
  • 索引不是越多越好:每个索引都会拖慢写入并占空间,入库量大的表要克制。

加分项:「知识库项目里我在 chunks 表上建了 (knowledge_base_id, status) 的复合索引配合向量索引——因为几乎所有检索都带这两个过滤条件,权限过滤和向量检索是一起发生的。」 别踩的坑:不要脱离查询模式谈索引。「给这个字段加个索引」这种答法太浅,要说清是哪个查询、执行计划怎么变。

Q4 · 事务隔离级别有哪些?你项目里怎么用的?

答题要点

  • 四级:读未提交、读已提交、可重复读、串行化,对应能不能出现脏读、不可重复读、幻读。
  • 默认值不一样:MySQL InnoDB 默认 RR(配合间隙锁,很大程度上避免幻读);PostgreSQL 默认 RC,PG 的 RR 是快照隔离(不会有幻读,但可能有写偏斜),串行化用 SSI 实现。
  • 我实际用得最多的是两个模式:悲观锁 SELECT ... FOR UPDATE(比如工单状态流转,锁住再改);乐观锁 版本号或 UPDATE ... WHERE status = 期望值(比如异步任务状态机的 CAS 更新)。
  • 长事务要避免:持有事务的时间越长,锁和 MVCC 版本堆积越严重。绝对不要在事务里等模型返回——一次 LLM 调用几秒钟,事务开着就是灾难。
  • 幂等靠唯一约束兜底比靠应用层判断可靠:先查再插在并发下必然重复,唯一索引 + 冲突处理才是确定的。

加分项:「『不要在事务里调外部服务』这条我是在 Agent 项目里体会最深的。工具调用动辄几秒,事务里等一次模型返回,数据库连接池很快就被占满了。」 别踩的坑:不要说「用串行化最安全」。串行化在高并发下会大量冲突回滚,性能代价很高,实际很少全局使用。 原理见 concurrency-transaction

Q5 · Redis 在你项目里做什么?缓存策略怎么设计?

答题要点

  • 会话记忆:客服和政务项目里,多轮对话历史放 Redis,用 list 或 hash 存,带 TTL 自动过期;要控制长度,做滑动窗口或摘要压缩,不能无限增长。
  • 任务队列:ARQ 就是基于 Redis 的,入库流水线的任务全走这里。限流和幂等INCR + 过期做计数限流,SET NX EX 做幂等标记和简单分布式锁(value 放唯一标识,释放时用 Lua 校验后再删)。zset 可以做滑动窗口限流,Stream 加消费者组能做带 ack 的队列。
  • 热点结果缓存:高频问题的检索结果和回答缓存起来,string + TTL,key 里带上模型版本和知识库版本,版本变了 key 自然失效。更新用 cache-aside:写库之后删缓存,不是改缓存。
  • 三类经典问题的对策:穿透(查不存在的 key)用空值缓存或布隆过滤器;击穿(热点 key 过期瞬间)用互斥重建或逻辑过期异步刷新;雪崩(大量 key 同时过期或 Redis 故障)靠 TTL 随机抖动 + 缓存挂了能直接走数据库的降级预案。
  • RAG 场景有一层特别的考虑:问答缓存要不要考虑语义相似。完全一致才命中,命中率低;做语义缓存又有风险——两个措辞相近的问题答案可能不同。我做得比较保守,只缓存精确匹配 + 相同知识库版本。

加分项:「缓存 key 里带版本号这件事很重要。切分策略改了、Embedding 模型换了,旧缓存必须整体失效——靠手动清缓存迟早会漏。别踩的坑:不要把 Redis 当数据库。它的持久化不是强一致的,会话这类可重建的数据放 Redis 没问题,审计日志、Checkpoint 这些必须落库。 原理见 redis-deep

6. 异步任务与部署

对应简历:「异步任务、Docker、Linux」,以及项目里的 ARQ 入库流水线和 vLLM 内网部署。

Q1 · 为什么选 ARQ 而不是 Celery?

答题要点

  • ARQ 是 asyncio 原生的:我的服务本身就是异步的,任务函数里要调异步的 httpx、异步的 SQLAlchemy,用 ARQ 不需要在同步和异步之间来回桥接。
  • 它只依赖 Redis,而 Redis 我本来就有(会话、缓存都在用),不用为了任务队列再引入 RabbitMQ。代码量小,出问题能直接看源码。
  • Celery 的优势是生态成熟、支持复杂工作流编排(chain、group、chord)、有 beat 定时和丰富的监控。如果需要多语言接入或复杂 DAG,我会选 Celery。
  • 我们的入库流水线是「解析 → 分块 → Embedding → 入库」的简单串行加重试,ARQ 的能力足够。
  • 结论:选型看的是「我的任务形态需要什么」,不是哪个更强大。 引入一个用不上的重型组件,运维成本是白付的。

加分项:「ARQ 的 job_id 我用得比较关键——它天然支持用自定义 job_id 做去重,我拿文件内容的哈希当 job_id,同一个文件重复上传就不会重复入库。」 别踩的坑:不要说「Celery 太重了不好」。它是行业标准,很多团队在用;说清你的场景为什么不需要它的能力就行。 原理见 background-workermessage-queue

Q2 · 任务幂等怎么做?

答题要点

  • 前提认知:消息队列基本都是 at-least-once,重复投递是常态,幂等必须在消费端做。
  • 第一层是业务唯一键:文档入库我用文件内容哈希加知识库 ID 作为唯一键,重复提交直接命中已有记录。
  • 第二层是数据库唯一约束:靠唯一索引 + 冲突处理(upsert),而不是「先查再插」——后者在并发下必然重复。
  • 第三层是状态机 CAS:任务状态从 pending 改成 processing 时带上期望的旧状态,更新影响行数为 0 就说明别人已经在处理,直接退出。
  • 分步骤的流水线要做到步骤级幂等:解析完了但 Embedding 失败,重试时应该跳过解析而不是重头来。所以每一步的产物都要落地并标记完成。

加分项:「我的原则是幂等要落在存储层,不能只在应用层判断。应用层的检查和写入之间总有一个窗口,并发下一定会漏。」 别踩的坑:不要说「加个 Redis 分布式锁就幂等了」。锁会超时、会释放,锁只能降低并发概率,最终一致要靠唯一约束。

Q3 · 任务失败了怎么处理?重试策略怎么定?

答题要点

  • 第一件事是给错误分类:可重试(网络超时、限流、模型 5xx、临时不可用)和不可重试(文件损坏解析失败、参数非法、权限不足)。不可重试的错误重试一百次也一样
  • 可重试的用指数退避加随机抖动,设最大次数。抖动是为了避免大批任务在同一时刻一起重试把下游打死。
  • 超过最大次数进失败队列或失败表,记录完整错误信息,支持人工查看和手动重跑,不能默默消失。
  • 部分成功要能续跑:一个文档一千个分块,写到第八百个失败,重试应该从八百开始。所以入库我是分批提交并记录进度的。
  • 状态要对用户可见:文档处理到哪一步、为什么失败,前端能看到。「一直显示处理中」是最差的失败方式。

加分项:「我们最常见的失败类型是扫描版 PDF 解析失败。这类重试没有意义,所以我直接把它标成需要人工处理,并在界面上提示用户这个文件需要转换后重新上传——把不可重试的错误尽快还给人,比在后台反复试有用。别踩的坑:不要说「失败就重试三次」。没有错误分类的重试策略,在面试官眼里等于没有策略。

Q4 · 你的服务怎么打包部署?Docker 有什么注意点?

答题要点

  • 多阶段构建:构建阶段装依赖,运行阶段只拷贝需要的产物,镜像体积能小很多;基础镜像用 slim 一类的精简版。
  • .dockerignore 一定要写(排掉 .git、缓存、本地数据),否则构建上下文巨大还可能把敏感文件打进去。
  • 依赖层和代码层分开:先拷依赖清单装依赖,再拷代码,代码改动不会让依赖层缓存失效,构建快很多。
  • 安全:用非 root 用户运行;配置和密钥通过环境变量或挂载注入,绝不写进镜像;镜像里不留任何凭证。
  • 运维配套:健康检查、日志输出到 stdout(交给外面的日志系统收集)、资源限制、compose 里用 healthcheck 控制启动依赖顺序。API 和 worker 用同一个镜像不同启动命令,保证代码版本一致。

加分项:「API 和 ARQ worker 我用同一个镜像、不同的 entrypoint。这样两边的依赖和代码版本天然一致——分开构建迟早会出现『worker 上的代码是旧的』这种事故。别踩的坑:不要在 Dockerfile 里 pip install 不带版本约束。构建不可复现,某天上游发新版本就炸。 原理见 docker-deployment

Q5 · vLLM 内网部署你做过什么?为什么用它?

答题要点

  • 用它的核心原因是吞吐:PagedAttention 把 KV cache 分页管理,显存碎片少;连续批处理(continuous batching)让新请求能插进正在跑的批次,并发下吞吐比朴素推理高很多。
  • 它提供 OpenAI 兼容的接口,这一点在工程上很关键:应用侧代码不用改,换模型就是换 base_url,本地和云端可以无缝切换。
  • 部署时要调的几个参数:显存占用比例、最大上下文长度、张量并行度(多卡)、是否开启前缀缓存(系统提示词很长时收益明显)。
  • 内网的额外约束:依赖和模型权重要提前离线准备、没有热更新、显卡资源固定,所以要提前规划并发上限和超时降级。
  • DeepSeek-R1 这类推理模型有个实际问题:输出很长的思考过程,对结构化输出和工具调用不友好,需要剥离思考内容并对 JSON 做校验重试。

加分项:「这段经历让我明白**『能不能私有化』是架构级决策,不是部署阶段的事**。没有外网、没有热更新、显存固定,这些约束会一路影响到你怎么设计降级和缓存。」 别踩的坑:不要把 vLLM 说成「一个模型」。它是推理引擎/服务框架,模型是另一回事。

7. 前端衔接(你的差异化优势)

对应简历:「前端开发:React、TypeScript、数据可视化及 Agent 流式交互界面。」

这一节要主动打出去

大部分 Python Agent 候选人到这里就没话说了。你能把流式界面从协议设计到渲染细节讲一遍, 这是你在同批候选人里最难被替代的一段。面试官问「你还有什么想补充的」,就讲这个。

Q1 · React 里怎么消费 SSE?

答题要点

  • 浏览器原生 EventSource 最简单,自带重连和 Last-Event-ID,但有两个硬限制:只能 GET,不能自定义请求头。带 Authorization 的场景就不好用。
  • 所以我实际用的是 fetch + ReadableStream:拿 response.body.getReader(),用 TextDecoder 增量解码,自己按 \n\n 切事件帧。
  • 有个必须处理的细节:一个 chunk 不一定是完整的事件帧。要维护一个缓冲区,把不完整的尾巴留到下一次拼接,否则高频推送时会出现 JSON 解析错误。
  • AbortController 支持用户中断生成;组件卸载时也要 abort,不然会内存泄漏和状态更新报警。
  • 重连要自己实现:记录已收到的事件位置,重连时告诉服务端从哪继续,或者干脆让这一轮失败并给重试按钮。不要静默重连然后重复渲染。

加分项:「我定的事件协议把 token 增量、阶段状态、工具调用、引用、错误、结束分成了不同类型。前端按类型分别渲染,不用去解析一个大 JSON 里的状态字段——这是我做过前端才会在意的设计。」 别踩的坑:不要说「用 EventSource 就行」。带鉴权的生产环境基本都得用 fetch 流式,答不出这一点说明没真做过。

Q2 · 流式渲染的性能问题怎么解决?

答题要点

  • 核心问题是更新频率:token 级推送可能每秒几十次,每次都 setState 触发整棵子树重渲染,长回答到后面会明显卡。
  • 我的做法是批量刷新:token 先进缓冲区,用 requestAnimationFrame 或固定间隔(几十毫秒)合并成一次更新。视觉上没差别,重渲染次数降一个量级。
  • 结构上把流式区域隔离成独立组件,避免整页重渲染;长列表(历史消息)要虚拟化。
  • Markdown 增量渲染有个特有的坑:流到一半的语法是不完整的——代码块没闭合、表格只有一行。要么容错渲染,要么代码块这类结构等闭合再渲染。
  • 自动滚动要做「用户向上滚动就停止自动跟随」,否则用户想看前面的内容时会被一直拽到底部。

加分项:「批量刷新的间隔我是按感知调的:太长会有卡顿感,太短没有性能收益。这个体验细节后端出身的人一般不会去调——它决定了产品『看起来聪明不聪明』。另外阶段事件(正在检索、检索到几条、正在生成)我一定会渲染出来,Agent 响应比普通接口慢得多,什么都不显示用户会以为卡死;工具调用要显示业务名称『正在查询订单』而不是函数名。」 别踩的坑:不要说「React 会自动批处理所以没问题」。自动批处理解决的是同一事件里的多次更新,流式是持续到达的独立更新,不在一个批次里。

Q3 · 数据可视化这块你做过什么?

答题要点

  • BI 项目里我做的是「结果自动出图」这一段:根据查询结果的结构选图表类型——时间序列走折线、分类对比走柱状、占比走饼图,多维度的给表格加下钻。
  • 图表类型可以让模型建议,但最终渲染要走确定性的规则兜底:字段类型、维度个数、数据量决定能画什么,模型建议的图表如果不匹配就降级成表格。
  • 数据量大要处理:降采样或聚合后再渲染,几万个点直接画会卡;量大时用 canvas 而不是 svg。
  • 前端我用过 React 配合图表库,也做过复杂表单和大表格,这些是我在品茗那三年的主业。
  • 还要做导出:表格导 Excel、图表导图片,这是 BI 场景的刚需。

加分项:「我坚持图表选择要有规则兜底。模型建议一个饼图去画时间序列的情况是真会发生的,出图是给业务做决策看的,不能是概率性正确。别踩的坑:不要把可视化说成「调个图表库」。这一段的价值在「从数据结构推断合适的表达方式」,那才是判断力。

附:面试前最后一遍

如果只剩十分钟,只看这几条

  1. 事件循环主动和 JS 对比,把前端出身打成优势(§1 Q1)。
  2. 「能用规则确定的,不要交给模型」——这句话在 Text2SQL 校验、HITL 分级、政策有效期过滤三个地方都能用,是你最强的一条主线判断。
  3. 每个技术选择都能说出代价:pgvector 省了一致性成本但有规模上限,混合检索提了召回但多一套系统,HITL 保了安全但慢了流程。
  4. 所有数字先过 11-数字口径待核实,宁可说量级,不要说精确值。
  5. 最后一定要反问10-口述稿 §5),问评测、失败率和兜底闭环。