Skip to content

一面基础:概念与原理(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、固定业务流程开放式问题、探索性任务、创意生成

纯流程固定场景不做多智能体的原因:

  1. 成本浪费:每个 Agent 决策都需要 LLM 推理,固定流程不需要 LLM 做路由
  2. 不确定性引入风险:多 Agent 协作的自主性意味着每次执行路径可能不同,固定流程需要 100% 确定性
  3. 延迟不可接受:Agent 间通信 + LLM 推理的延迟叠加,对固定流程是纯开销
  4. 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模型先生成多个推理链,再自选最优需要高准确率的单步推理计算成本翻倍、延迟增加
ReActThought → 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 提升吞吐量的三个核心原因:

  1. PagedAttention:显存利用率从 ~40% 提升到 ~96%,几乎无碎片
  2. Continuous Batching:动态拼批,请求完成即释放资源,新请求即时加入,GPU 利用率最大化
  3. Prefix Caching:相同系统 Prompt 的请求共享前缀 KV Cache,减少重复计算

加分点: "vLLM 在我们的场景下 QPS 提升了 3-4 倍。关键是 PagedAttention 解决的不仅是显存浪费,还支持了灵活的批处理调度,这两者叠加效果才显著。"


7. GIL 什么时候释放?IO 密集型/CPU 密集型分别用多进程还是多线程? ​

分档:一面基础

高分答题思路:

GIL(全局解释器锁)释放时机:

  1. I/O 操作:文件读写、网络请求、数据库查询等阻塞 I/O 会主动释放 GIL
  2. 定时释放:Python 3.x 默认每 5ms 检查一次,主动让出 GIL 给其他线程
  3. C 扩展中手动释放:C 代码中通过 Py_BEGIN_ALLOW_THREADS / Py_END_ALLOW_THREADS 释放
  4. time.sleep():会释放 GIL

选型原则:

场景选择原因
I/O 密集型多线程 / asyncioI/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 事件循环底层原理:

  1. 核心组件:事件循环(Event Loop) + 任务队列(Task Queue) + 就绪回调队列(Ready Queue)
  2. epoll/kqueue 驱动:底层使用 OS 的 epoll(Linux)/ kqueue(macOS)/ IOCP(Windows)监听 I/O 事件
  3. 执行流程:
    • 注册 I/O 事件 → epoll 监听 → I/O 就绪 → 回调加入 Ready Queue → 事件循环取出执行
    • 每次循环:执行所有 Ready Queue 中的回调 → 调用 epoll.poll(timeout) 等待新事件 → 重复
  4. 协程调度: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. 多头注意力作用,单头注意力有什么缺陷? ​

分档:一面基础

高分答题思路:

单头注意力的缺陷:

  1. 表达力受限:一组 Q/K/V 只能学习一种注意力模式(如只能关注语法关系 OR 语义关系,不能同时)
  2. 瓶颈问题:所有信息压缩到一个注意力分布中,不同类型的关系相互竞争
  3. 容量不足:单头难以同时捕捉局部和全局的依赖关系

多头注意力的优势:

  1. 多视角:不同头可以学习不同的注意力模式(语法关系、语义相似度、位置关系等)
  2. 并行计算:多个头可以并行计算,不增加计算延迟
  3. 更高表达力:通过拼接多个头的输出,模型获得更丰富的表示

实际观察:

  • 研究表明某些头专注于语法依赖、某些头关注相邻 token、某些头关注长距离依赖
  • 多头冗余问题:部分头的作用高度重叠,可以通过剪枝减少头数而不影响效果

加分点: "我们的实践中,推理时做了 Head 剪枝——去掉 30% 冗余头,效果几乎无损但推理加速 15%。这和生产中的模型部署优化直接相关。"


11. Encoder 和 Decoder 结构区别,哪些 Agent 场景只用 Encoder、哪些必须 Decoder? ​

分档:一面基础

高分答题思路:

结构区别:

维度EncoderDecoder
注意力类型双向自注意力单向自注意力(Masked)+ 交叉注意力
可见范围每个位置可以看到全部输入每个位置只能看到当前位置及之前
输出输入的深度表示逐 token 自回归生成
典型模型BERT、RoBERTaGPT 系列、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 工具加埋点:

python
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 等验证

隔离复杂三方接口的测试方案:

python
# 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 代码块标记再解析
python
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 校验更可维护。"


答题策略总结 ​

要点说明
先定义后展开先用一句话给出核心答案,再展开细节
结构化输出用表格/列表组织对比信息,面试官更容易抓住要点
举例加分每个概念尽量举一个实际场景或代码片段
主动设界基础题不过度展开,点到为止,留钩子等面试官追问
展示深度在加分点主动提到生产实践,展示真实经验