一面基础:概念与原理(15题)
考察重点:概念清晰度、基本原理理解、技术广度 答题要求:答出"是什么 + 为什么 + 适用场景"即可,不需深入实战细节 建议每题答题时间:2-3分钟
一、Agent 架构基础
1. LangGraph、AutoGen、CrewAI、自研状态机各自优缺点,什么场景坚决不用 LangGraph?
分档:一面基础
高分答题思路:
| 框架 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|
| LangGraph | 状态机模型清晰、支持循环/条件分支、可视化编排、与 LangChain 生态深度集成 | 学习曲线陡、复杂图编排 Debug 困难、过度封装导致定制灵活性差、性能开销大 | 中等复杂度的多步骤 Agent 流程、需要可视化编排 |
| AutoGen | 多 Agent 对话协作自然、代码执行能力内置、微软生态支持 | 对话式协作控制力弱、难以精确控制流程、消息爆炸问题 | 研究探索型多 Agent 协作、代码生成与验证 |
| CrewAI | 角色化 Agent 定义直观、任务分配语义清晰、上手快 | 功能较薄、复杂场景扩展性不足、社区生态小 | 快速原型、角色明确的简单多 Agent 场景 |
| 自研状态机 | 完全可控、性能最优、可深度定制、无框架依赖 | 开发成本高、需要自建编排/监控/容错全套能力、维护负担重 | 生产级高并发场景、有特殊性能/定制需求 |
坚决不用 LangGraph 的场景:
- 高并发线上服务(框架开销 + GIL 瓶颈)
- 流程高度固定、无分支循环的简单 Pipeline(杀鸡用牛刀)
- 需要精细控制每一步执行逻辑和错误处理的场景(LangGraph 的抽象层反而阻碍)
- 对延迟敏感的实时场景(图编排 + 状态序列化开销)
加分点: 提到自研时可以说"我们借鉴了 LangGraph 的图编排思想,但用 Python 原生协程 + 状态枚举实现,去掉了框架开销,单次调度延迟降低 40%"。
2. 自研 Agent 状态机如何解决循环死锁、任务无限重试问题?有做状态持久化吗?
分档:一面基础
高分答题思路:
循环死锁解决方案:
- 最大迭代次数硬限制:每个状态节点设置 max_retries=3,超限后进入 fallback 状态
- 状态超时机制:每个状态执行有 timeout,超时后触发状态转移或降级
- 环检测:记录状态转移路径,检测到重复状态序列时主动 break,进入人工介入或 fallback
- 优先级抢占:高优先级任务可以抢占死锁状态的资源
无限重试解决方案:
- 指数退避重试:retry_interval = base * 2^n,同时设最大重试次数
- 熔断机制:连续失败 N 次后熔断该工具/模型,一段时间内直接走降级
- 死信队列:重试耗尽的任务进入死信队列,等待人工处理或批量补偿
状态持久化:
- 轻量级:Redis 存状态机快照(state + context + timestamp)
- 重量级:PostgreSQL/MySQL 存完整执行轨迹,支持断点续跑
- 关键设计:状态序列化用 JSON Schema 约束,版本号管理防止 schema 变更导致反序列化失败
加分点: 提到"状态持久化不只是为了容错,也是为了 Badcase 回溯——线上问题可以直接 replay 状态机执行轨迹定位问题节点"。
3. Workflow 编排和多 Agent 自主协作怎么取舍?纯流程固定场景为什么不做多智能体?
分档:一面基础
高分答题思路:
取舍核心原则:流程确定性 vs 灵活性
| 维度 | Workflow 编排 | 多 Agent 自主协作 |
|---|---|---|
| 流程确定性 | 流程固定、分支可枚举 | 流程动态、需要自主决策 |
| 可控性 | 高(每步可审计) | 低(Agent 自主选择路径) |
| 成本 | 低(不需要 LLM 做路由决策) | 高(每次协作都需要 LLM 推理) |
| 延迟 | 低(确定性执行) | 高(多轮 LLM 调用) |
| 适用场景 | 数据处理 Pipeline、固定业务流程 | 开放式问题、探索性任务、创意生成 |
纯流程固定场景不做多智能体的原因:
- 成本浪费:每个 Agent 决策都需要 LLM 推理,固定流程不需要 LLM 做路由
- 不确定性引入风险:多 Agent 协作的自主性意味着每次执行路径可能不同,固定流程需要 100% 确定性
- 延迟不可接受:Agent 间通信 + LLM 推理的延迟叠加,对固定流程是纯开销
- Debug 困难:固定流程出问题可以直接定位到步骤,多 Agent 链路追踪复杂
加分点: "我们的实践是:70% 场景用 Workflow + 规则路由,30% 需要灵活理解的场景才引入 Agent 决策。混合架构:Workflow 做骨架,关键决策点嵌入 Agent。"
4. Agent 记忆体系分短期/长期/会话记忆,分别怎么存储、淘汰、检索?
分档:一面基础
高分答题思路:
| 记忆类型 | 存储方案 | 淘汰策略 | 检索方式 |
|---|---|---|---|
| 短期记忆 | LLM Context Window(直接拼在 Prompt 中) | 滑动窗口(保留最近 N 轮)/ 摘要压缩 | 无需检索,直接在上下文中 |
| 会话记忆 | Redis(KV 存储,session_id 为 key) | TTL 过期(如 24h)+ 会话结束时持久化 | 按 session_id 直接读取 |
| 长期记忆 | 向量数据库(Milvus/Faiss)+ 结构化存储(PostgreSQL) | LRU + 重要性评分衰减 + 定期归档 | 向量相似度检索 + 关键词倒排索引混合 |
检索策略进阶:
- 混合检索:向量相似度(语义) + BM25(关键词) + 时间衰减因子
- 分层检索:先检索会话记忆 → 再检索长期记忆 → 最后补充知识库
- 记忆压缩:定期对短期记忆做摘要,提取关键信息写入长期记忆
加分点: 提到"记忆不只是存文本,还要存结构化信息——用户偏好、决策历史、工具调用结果。我们用 JSON Schema 对记忆条目做结构化,方便后续精确查询"。
5. 区分 Few-shot、CoT、Self-CoT、ReAct、Plan-Solve 几种提示范式,各自适用场景?
分档:一面基础
高分答题思路:
| 范式 | 核心思想 | 适用场景 | 局限 |
|---|---|---|---|
| Few-shot | 给几个示例让模型模仿 | 输出格式约束、简单分类任务 | 示例选择敏感、占用上下文 |
| CoT | "Let's think step by step" 引导逐步推理 | 数学计算、逻辑推理、多条件判断 | 推理过程不可控、可能产生错误中间步 |
| Self-CoT | 模型先生成多个推理链,再自选最优 | 需要高准确率的单步推理 | 计算成本翻倍、延迟增加 |
| ReAct | Thought → Action → Observation 循环 | 需要调用外部工具的场景、信息检索 | 工具调用次数多时上下文膨胀、可能陷入循环 |
| Plan-Solve | 先制定计划再逐步执行 | 复杂多步任务、项目管理类任务 | 计划可能脱离实际、执行偏差时需重新规划 |
实际应用建议:
- 简单格式约束 → Few-shot
- 纯推理问题 → CoT
- 需要工具调用 → ReAct
- 复杂多步任务 → Plan-Solve + ReAct 组合(先规划再逐步 ReAct 执行)
加分点: "我们在生产中用的是 Plan-Solve + ReAct 混合范式:先用 Plan-Solve 生成任务分解计划,每个子任务用 ReAct 执行。这样既有全局规划,又有局部灵活性。"
二、LLM 工程基础
6. KV Cache 复用、分页 KV Cache 原理,vLLM 相比原生 Transformers 提升吞吐量的核心原因?
分档:一面基础
高分答题思路:
KV Cache 原理:
- Transformer 自回归生成时,每生成一个 token 都需要重新计算 Attention
- 但前面 token 的 Key/Value 矩阵不变,可以缓存复用,避免重复计算
- 空间换时间:用 GPU 显存存储历史 K/V,新 token 只需计算自己的 Q 与已有 K/V 做 Attention
分页 KV Cache(PagedAttention,vLLM 核心创新):
- 传统方式:为每个请求预分配最大序列长度的连续显存 → 浪费严重
- PagedAttention:借鉴 OS 虚拟内存分页机制,将 KV Cache 按固定大小 Block 存储
- 物理块不连续,逻辑上通过 Block Table 映射
- 按需分配,用多少给多少,不预分配
- 支持写时复制(Copy-on-Write),多个请求可共享相同前缀的 KV Cache
vLLM 提升吞吐量的三个核心原因:
- PagedAttention:显存利用率从 ~40% 提升到 ~96%,几乎无碎片
- Continuous Batching:动态拼批,请求完成即释放资源,新请求即时加入,GPU 利用率最大化
- Prefix Caching:相同系统 Prompt 的请求共享前缀 KV Cache,减少重复计算
加分点: "vLLM 在我们的场景下 QPS 提升了 3-4 倍。关键是 PagedAttention 解决的不仅是显存浪费,还支持了灵活的批处理调度,这两者叠加效果才显著。"
7. GIL 什么时候释放?IO 密集型/CPU 密集型分别用多进程还是多线程?
分档:一面基础
高分答题思路:
GIL(全局解释器锁)释放时机:
- I/O 操作:文件读写、网络请求、数据库查询等阻塞 I/O 会主动释放 GIL
- 定时释放:Python 3.x 默认每 5ms 检查一次,主动让出 GIL 给其他线程
- C 扩展中手动释放:C 代码中通过
Py_BEGIN_ALLOW_THREADS/Py_END_ALLOW_THREADS释放 - time.sleep():会释放 GIL
选型原则:
| 场景 | 选择 | 原因 |
|---|---|---|
| I/O 密集型 | 多线程 / asyncio | I/O 等待时释放 GIL,其他线程可执行;线程切换开销小 |
| CPU 密集型 | 多进程 | GIL 限制单进程内只有一个线程执行 Python 字节码,多进程绕过 GIL |
| 混合型 | 多进程 + 每进程内多线程/协程 | 进程间分配 CPU 密集任务,进程内用协程处理 I/O |
Agent 场景实际应用:
- Agent 大量调用 LLM API(I/O 密集)→ asyncio 协程最优
- 本地模型推理(CPU/GPU 密集)→ 多进程或直接用 GPU 推理框架
- 批量 Embedding 计算 → 多进程 + GPU 批处理
加分点: 提到 Python 3.13 的 No-GIL(PEP 703)实验性支持,展示对语言演进的关注。
8. 进程、线程、协程区别;asyncio 事件循环底层原理?
分档:一面基础
高分答题思路:
三者对比:
| 维度 | 进程 | 线程 | 协程 |
|---|---|---|---|
| 资源 | 独立内存空间 | 共享进程内存 | 共享线程内存 |
| 切换开销 | 最大(内核态 + 上下文 + 内存空间) | 中等(内核态 + 上下文) | 最小(用户态 + 保存寄存器/栈) |
| 通信方式 | IPC(管道/队列/共享内存) | 共享变量(需加锁) | 同步变量(单线程内无锁) |
| 数量上限 | 几十~几百 | 几百~几千 | 几万~几十万 |
| 调度者 | 操作系统内核 | 操作系统内核 | 用户态事件循环 |
asyncio 事件循环底层原理:
- 核心组件:事件循环(Event Loop) + 任务队列(Task Queue) + 就绪回调队列(Ready Queue)
- epoll/kqueue 驱动:底层使用 OS 的 epoll(Linux)/ kqueue(macOS)/ IOCP(Windows)监听 I/O 事件
- 执行流程:
- 注册 I/O 事件 → epoll 监听 → I/O 就绪 → 回调加入 Ready Queue → 事件循环取出执行
- 每次循环:执行所有 Ready Queue 中的回调 → 调用 epoll.poll(timeout) 等待新事件 → 重复
- 协程调度:
async def函数被编译为协程对象,await相当于 yield 控制权回到事件循环,事件循环再调度其他就绪的协程
加分点: "我们的 Agent 后端用 FastAPI + asyncio,单个 Worker 可以同时处理几百个并发 LLM 请求,瓶颈不在并发数而在 LLM API 的 rate limit。"
9. Softmax 为什么需要除以 √dk,解决什么问题?
分档:一面基础
高分答题思路:
问题: 当 dk(Key 维度)较大时,点积 Q·K 的数值方差随 dk 线性增长(Var(Q·K) = dk · Var(Q) · Var(K)),导致 Softmax 输入值过大。
过大带来的问题:
- Softmax 梯度趋近于 0(梯度消失),导致训练不稳定
- 输出分布变成 one-hot(最大值对应的概率趋近 1,其他趋近 0),信息丢失
除以 √dk 的效果:
- 将点积的方差缩放回 Var(Q·K)/dk = Var(Q) · Var(K),与 dk 无关
- 保持梯度在合理范围内,训练稳定
本质理解: 这是一个方差归一化操作,确保无论 dk 多大,Attention 的输入分布都保持稳定。
加分点: 提到"除以 √dk 是在计算效率和效果之间的折中——也可以用其他归一化方式(如 LayerNorm),但 √dk 计算最简单且效果足够好"。
10. 多头注意力作用,单头注意力有什么缺陷?
分档:一面基础
高分答题思路:
单头注意力的缺陷:
- 表达力受限:一组 Q/K/V 只能学习一种注意力模式(如只能关注语法关系 OR 语义关系,不能同时)
- 瓶颈问题:所有信息压缩到一个注意力分布中,不同类型的关系相互竞争
- 容量不足:单头难以同时捕捉局部和全局的依赖关系
多头注意力的优势:
- 多视角:不同头可以学习不同的注意力模式(语法关系、语义相似度、位置关系等)
- 并行计算:多个头可以并行计算,不增加计算延迟
- 更高表达力:通过拼接多个头的输出,模型获得更丰富的表示
实际观察:
- 研究表明某些头专注于语法依赖、某些头关注相邻 token、某些头关注长距离依赖
- 多头冗余问题:部分头的作用高度重叠,可以通过剪枝减少头数而不影响效果
加分点: "我们的实践中,推理时做了 Head 剪枝——去掉 30% 冗余头,效果几乎无损但推理加速 15%。这和生产中的模型部署优化直接相关。"
11. Encoder 和 Decoder 结构区别,哪些 Agent 场景只用 Encoder、哪些必须 Decoder?
分档:一面基础
高分答题思路:
结构区别:
| 维度 | Encoder | Decoder |
|---|---|---|
| 注意力类型 | 双向自注意力 | 单向自注意力(Masked)+ 交叉注意力 |
| 可见范围 | 每个位置可以看到全部输入 | 每个位置只能看到当前位置及之前 |
| 输出 | 输入的深度表示 | 逐 token 自回归生成 |
| 典型模型 | BERT、RoBERTa | GPT 系列、LLaMA |
Agent 场景对应:
| 场景 | 选择 | 原因 |
|---|---|---|
| 意图分类、情感分析 | 只需 Encoder | 输入完整可见,不需要生成,Encoder 的双向注意力效果更好 |
| 向量检索/Embedding | 只需 Encoder | 需要输入的整体表示,双向注意力更合适 |
| 文本生成、对话回复 | 必须 Decoder | 自回归生成需要因果 Mask 防止看到未来 token |
| 代码生成、工具调用 | 必须 Decoder | 需要生成结构化输出 |
| 翻译、摘要 | Encoder-Decoder(如 T5/BART) | 先理解完整输入(Encoder),再生成输出(Decoder) |
加分点: "现代大模型(GPT-4、LLaMA)基本都是 Decoder-only 架构,因为 Scaling Law 表明 Decoder-only 在大规模下效果最好。但对于 Embedding 和分类任务,BERT 类 Encoder 模型仍然更优。"
12. Token Embedding 和 Positional Encoding 作用,为什么原生 Self-Attention 没有位置感知?
分档:一面基础
高分答题思路:
Token Embedding:
- 将离散的 token ID 映射为连续的稠密向量(如 d_model=4096)
- 语义相近的 token 在向量空间中距离更近
- 是模型可学习的参数矩阵(vocab_size × d_model)
Positional Encoding:
- 给每个位置一个独特的位置信号,让模型区分相同 token 在不同位置的语义差异
- 两种主流方案:
- 绝对位置编码(Sinusoidal / Learnable):每个位置有固定编码
- 相对位置编码(RoPE/ALiBi):通过旋转或偏移编码相对距离,泛化性更好
原生 Self-Attention 没有位置感知的原因:
- Self-Attention 的计算是 Q·K^T,本质上是集合上的操作(Permutation Equivariant)
- 打乱输入顺序,每个 token 的输出不变(只是输出顺序也打乱了)
- 模型无法区分"猫追狗"和"狗追猫"——所以必须引入位置信息
加分点: "RoPE(旋转位置编码)是目前主流方案(LLaMA/GPT-4 都用),它通过对 Q/K 做旋转变换编码相对位置,优势是可以外推到训练时未见过的序列长度。"
13. 装饰器、元类底层实现;如何用元类统一给 Agent 工具加埋点日志?
分档:一面基础
高分答题思路:
装饰器底层:
- 装饰器本质是高阶函数:接收函数作为参数,返回新函数
- 语法糖
@decorator等价于func = decorator(func) - 底层实现:闭包 +
functools.wraps保留元信息
元类底层:
- 元类是"类的类"——
type是所有类的元类 - 类创建时,Python 先调用
metaclass.__new__创建类对象,再调用metaclass.__init__初始化 - 可以在类创建时拦截并修改类属性、方法
用元类给 Agent 工具加埋点:
class ToolMeta(type):
def __new__(cls, name, bases, namespace):
# 遍历类中所有方法
for key, val in namespace.items():
if callable(val) and not key.startswith('_'):
# 用装饰器包装每个工具方法
namespace[key] = log_wrapper(val)
return super().__new__(cls, name, bases, namespace)
def log_wrapper(func):
@functools.wraps(func)
async def wrapper(*args, **kwargs):
start = time.time()
logger.info(f"[Tool] {func.__name__} called, args={args}")
try:
result = await func(*args, **kwargs)
logger.info(f"[Tool] {func.__name__} done, cost={time.time()-start:.3f}s")
return result
except Exception as e:
logger.error(f"[Tool] {func.__name__} error: {e}")
raise
return wrapper
# 使用:所有继承 ToolMeta 的类,其公开方法自动加埋点
class SearchTool(metaclass=ToolMeta):
def search(self, query): ...
def filter(self, results): ...加分点: 提到"实际生产中我们用更轻量的方式——装饰器 + 注册器模式,而非元类。元类虽然优雅但 Debug 困难、IDE 支持差,团队协作中理解成本高"。
14. Mock 原理:unittest.mock 如何拦截函数调用;依赖复杂三方接口怎么隔离测试?
分档:一面基础
高分答题思路:
unittest.mock 原理:
Mock对象通过__getattr__和__call__魔术方法,动态创建任意属性和方法patch装饰器/上下文管理器:通过sys.modules或对象属性替换,临时将目标对象替换为 Mock 对象- 原理:在
__enter__时保存原对象引用并替换,__exit__时恢复 - 支持
assert_called_with、return_value、side_effect等验证
隔离复杂三方接口的测试方案:
# 1. patch 替换外部依赖
@patch('agent.tools.search_api.call_external_search')
def test_agent_search(mock_search):
mock_search.return_value = {"results": [{"title": "test"}]}
agent = MyAgent()
result = agent.handle("搜索测试内容")
assert "测试内容" in result
mock_search.assert_called_once()
# 2. 录制-回放模式(responses 库)
@responses.activate
def test_llm_call():
responses.add(responses.POST, "https://api.openai.com/v1/chat/completions",
json={"choices": [{"message": {"content": "mocked"}}]})
# 测试代码...
# 3. 依赖注入:接口与实现分离
class Agent:
def __init__(self, llm_client: LLMClient):
self.llm = llm_client # 测试时注入 MockLLMClient隔离策略:
- LLM 调用 → Mock 返回固定响应,测试 Agent 逻辑而非 LLM 能力
- 数据库/缓存 → 用测试专用实例或内存数据库
- 外部 API →
responses/httpx-mock录制回放 - 时间相关 →
freezegun冻结时间
加分点: "我们对 LLM 调用做了录制-回放机制:首次调用真实 LLM 并录制响应,后续测试回放录制结果。既保证真实性又避免 API 费用和延迟。"
15. 结构化输出约束:怎么强制 LLM 输出标准 JSON,解决格式乱码、字段缺失问题?
分档:一面基础
高分答题思路:
多层次保障方案:
| 层次 | 方案 | 效果 |
|---|---|---|
| Prompt 层 | System Prompt 明确指定 JSON Schema + Few-shot 示例 | 提升格式准确率到 ~85% |
| 解码层 | JSON Mode(OpenAI)/ Outlines / Guidance 强制 JSON 格式 | 格式准确率 ~99%,但内容质量可能下降 |
| 后处理层 | 正则提取 JSON 块 → json.loads → Schema 校验 → 字段补默认值 | 兜底保障,确保下游不报错 |
| 重试层 | 解析失败 → 追加 "请修正为标准JSON" 重新调用(最多 2 次) | 极端情况兜底 |
常见问题与解决:
- 格式乱码:Prompt 中用 ```json 代码块包裹 + 后处理正则提取
- 字段缺失:JSON Schema 定义 required 字段 + 后处理补默认值
- 嵌套 JSON:递归校验每一层结构
- Markdown 混入:后处理先 strip 代码块标记再解析
def safe_json_parse(llm_output: str, schema: dict) -> dict:
# 1. 尝试直接解析
try:
return json.loads(llm_output)
except:
pass
# 2. 正则提取 JSON 块
match = re.search(r'```json\s*(.*?)\s*```', llm_output, re.DOTALL)
if match:
try:
return json.loads(match.group(1))
except:
pass
# 3. 兜底返回默认结构
return {k: v.get("default") for k, v in schema["properties"].items()}加分点: "生产中我们用 Pydantic 做后处理校验——LLM 输出 → Pydantic Model 校验 → 自动类型转换 + 字段补默认值 + 清晰的错误信息。比手写 JSON Schema 校验更可维护。"
答题策略总结
| 要点 | 说明 |
|---|---|
| 先定义后展开 | 先用一句话给出核心答案,再展开细节 |
| 结构化输出 | 用表格/列表组织对比信息,面试官更容易抓住要点 |
| 举例加分 | 每个概念尽量举一个实际场景或代码片段 |
| 主动设界 | 基础题不过度展开,点到为止,留钩子等面试官追问 |
| 展示深度 | 在加分点主动提到生产实践,展示真实经验 |