字节AI Agent面试题大全及参考答案(下)

Skills 的原理有没有了解过?怎么实现的?MCP 和 Skill 的区别是什么?

Skill 是大模型 Agent 领域对能力单元的抽象,中文常译作技能,本质是把一类可被 Agent 调用的能力做标准化封装,包含工具调用、业务流程、提示词模板、子 Agent、查询逻辑等,不局限于单纯函数调用。面试加分点,不要把 Skill 等同于普通 function call 函数,要区分 Function、Skill、MCP 三者的边界,讲清楚 Skill 不止是单个函数,可以是复杂业务流程,同时说明 MCP 协议定位是通信协议,不是能力本身。

Skill 核心原理,将离散能力封装成具备元信息的独立单元,Agent 规划层只需要读取 Skill 的描述元数据,理解这个技能能干什么、入参是什么、输出格式、前置约束,由 Agent 自主选择是否启用、何时启用该 Skill。Skill 屏蔽底层实现细节,上层 Agent 不需要关心内部是调用 API、执行代码、还是嵌套一套子 Agent 工作流,只关心输入输出契约。和原生 Function Call 最大差异,Function Call 大多对应单个函数,Skill 可以是多个函数组合完成的一段完整业务流程。例如一个 "员工报销查询 Skill" 内部可能要做权限校验、调用数据库查询、结果格式化,封装成一个 Skill 对外暴露,Agent 只需要传入员工 ID、时间,拿到最终整理后的报销结果,感知不到内部多步操作。

Skill 工程实现分为元数据定义层、执行层、调度接入层。元数据定义,使用 JSON/YAML 描述 Skill 基础信息,包含技能名称、功能描述、入参 JSON Schema、出参格式、触发条件、权限要求、异常处理说明。执行层,Skill 内部可以编排函数调用、子 Agent、RAG 检索、提示词模板,内部逻辑可以很复杂,对外只暴露统一入口。调度接入层,Agent 规划模块加载全部 Skill 元数据,注入到上下文,由大模型决策是否调用;调度框架接收模型输出的调用指令,校验入参合法性、权限,转发给对应 Skill 执行,捕获异常,把执行结果整理后返回给 Agent 上下文。

简易 Skill 元数据示例

复制代码
{
    "skill_name":"query_expense",
    "description":"查询指定员工指定时间段报销数据",
    "input_schema":{
        "type":"object",
        "properties":{
            "staff_id":{"type":"string","description":"员工编号"},
            "time_range":{"type":"string","description":"查询时间范围"}
        },
        "required":["staff_id","time_range"]
    },
    "permission":["finance:read"],
    "exec_module":"skill.expense.ExpenseSkill"
}

MCP 即 Model Context Protocol,模型上下文协议,它是一套标准化通信协议规范,用来完成大模型客户端和外部能力服务之间的通信,MCP 本身不实现业务能力,只定义交互格式、消息格式、错误码、数据传输规范。Skill 是业务能力实体,MCP 是 Skill 对外暴露时可选用的通信协议。

对比维度 Skill MCP
本质 业务能力封装单元 通信传输协议标准
职责 实现具体业务逻辑,定义输入输出契约 定义消息格式、请求响应报文、异常、数据序列化
关系 Skill 可以基于 MCP 协议对外提供服务;Skill 也可以本地进程内直接调用,不使用 MCP MCP 协议用来传输 Skill 调用请求与返回结果,不包含业务逻辑
部署形态 可以本地内嵌,也可以远程服务 网络层协议,多用于远程跨进程调用

Skill 可以不依赖 MCP,进程内直接实例化运行;当 Skill 部署为独立远程服务时,可以采用 MCP 协议完成 Agent 主程序和 Skill 服务之间消息交互。很多人会混淆,把 MCP 当成能力,实际 MCP 解决的是 "怎么传消息",Skill 解决的是 "做什么事"。

记忆法选用实体协议区分记忆法:Skill 是业务能力单元,可封装复杂流程;MCP 是通信协议管报文传输;Skill 可以本地运行,远程部署可基于 MCP 通信。

针对 Agent 自进化,你的理解是什么?

Agent 自进化,指 Agent 在运行过程中,基于真实交互轨迹、失败案例、成功样本,自动或者半自动化迭代自身能力,不需要完全依靠人工反复修改提示词、工具、规划逻辑。面试加分点,区分自进化不等于模型 finetune 微调,自进化更多聚焦 Agent 侧:规划策略、提示词、Skill 调用逻辑、错误修复、案例沉淀,而不是修改基座大模型权重;同时讲清楚自进化完整闭环,以及现实约束,不存在完全全自动无人工参与的自进化。

传统 Agent 迭代方式是人工驱动,研发人员收集 bad case,人工分析,修改 system prompt、调整工具描述、补充 few‑shot 样例、修复 Skill 逻辑,重新发布版本。效率低,用户遇到的各类场景很难全部被研发覆盖。Agent 自进化希望把部分迭代流程自动化,形成闭环:采集交互轨迹→识别失败样本→根因分析→生成改进产物→验证改进效果→生效到 Agent。

Agent 自进化可以划分为几个层次。第一层是案例记忆层,属于轻量进化,把成功、失败交互案例提取出来,作为 Few‑Shot 样本存入向量库,后续遇到相似 query 自动召回对应历史案例注入上下文,优化当前推理,不修改代码、不修改提示词模板本体。第二层是提示词与策略进化,分析失败轨迹,自动修改 system prompt 约束、工具描述、To‑Do List 规划策略,生成新版本提示词,经过验证之后切换使用。第三层是 Skill 能力进化,针对反复失败的任务,自动生成或者优化 Skill 工具逻辑,修正参数校验逻辑,补充异常分支。第四层是模型微调进化,把高质量 Agent 轨迹整理成 SFT 数据集,做监督微调,修改基座模型权重,成本最高,一般作为可选高阶手段。

自进化不是无限自我变强,存在现实边界。第一,自进化依赖高质量交互轨迹,如果原始数据充满错误,进化只会不断放大错误;第二,必须要有校验环节,自动化生成的改进产物不能直接上线,要经过自动化评测、人工抽检,防止 Agent 自我修改之后能力退化;第三,危险场景不能完全交给自进化,涉及数据修改、高危操作,不允许自动生成与更新 Skill,只能人工审核之后才生效;第四,自进化会产生版本爆炸,每一轮进化产出新版本 Agent 配置,需要版本管理、回滚机制。

Agent 自进化闭环完整链路:用户交互产生完整 Agent 轨迹(用户 query、思考过程、工具调用、返回结果、最终回答、用户反馈);轨迹过滤筛选,过滤噪声、无效对话;根因定位,判断失败是规划错误、工具选择错误、提示词约束不足、Skill 内部 bug 还是知识库缺失;生成改进候选产物,例如新 Few‑Shot 样例、更新系统提示词、优化 Skill 描述;进入评测沙箱,批量跑测试集,对比改进前后指标;达标之后才可以灰度生效,不达标丢弃该改进产物。

记忆法选用闭环分层记忆法,自进化闭环:采集轨迹→根因分析→产出改进产物→沙箱验证→灰度生效;分层:案例记忆、提示词策略进化、Skill 进化、微调进化;自进化不等于完全全自动,需要校验管控。

Agent 自进化最后提取的产物,一是提取的标准是怎样确定的,二是提取内容的质量好坏如何评估?

Agent 自进化流程中,从原始交互轨迹提取产物,常见产物包含 Few‑Shot 样本、更新后的系统提示词片段、优化后的 Skill 描述、修正后的规划规则、子任务 To‑Do 模板。面试加分点,区分提取筛选标准和产物质量评估两套体系,筛选标准决定哪些轨迹可以进入提取环节,质量评估决定产出的产物能不能投入使用,很多面试容易把两者混在一起,同时要说明不能只要失败 case,部分高质量成功 case 同样需要提取。

提取标准,用来筛选原始 Agent 交互轨迹,只有满足标准的轨迹才会进入后续提取流程,分为基础过滤条件、业务标签条件、正负样本区分。基础过滤条件,首先过滤残缺轨迹,工具调用中断、网络异常、执行报错中断、用户中途放弃对话,这类不完整交互直接丢弃;过滤噪声对话,闲聊、乱输入、prompt 注入攻击样本;过滤 Skill 内部底层代码 bug,属于程序缺陷,不属于 Agent 策略层面问题,这类 case 不需要 Agent 自进化处理,交由研发修复代码。

业务标签条件,会给每条轨迹打上标签:成功样本、规划失败、工具选错、参数错误、幻觉、缺少约束、回答不符合格式。只有标签命中 Agent 策略类问题,才允许提取。Skill 本身代码 bug、知识库没有对应知识不属于自进化提取范围。同时设置用户反馈权重,用户点踩、明确指出回答错误的轨迹优先级升高;用户点赞的成功样本也纳入候选,用于沉淀优秀 Few‑Shot。

另外设置多样性标准,避免提取大量高度重复样本。如果已经提取过大量同类 case,同类新轨迹会降低优先级,防止产物集合大量重复,造成上下文 token 膨胀。会设置去重逻辑,基于 query 语义相似度,相似 query 只保留代表性样本。

产物提取阶段,大模型基于合格轨迹,提炼产出物,例如 Few‑Shot 样例会提取:用户问题、Agent 思考、工具调用、正确输出;提示词片段提取会提炼缺失的约束规则。

产物质量评估分为自动化评测、沙箱批量回归评测、人工抽样评测三层。自动化评测针对产物本身做检查,检查格式合法性,Few‑Shot 格式是否完整,提示词片段会不会产生冲突约束;检查产物是否和已有规则冲突,新提取的约束不能和原有 system prompt 矛盾;检查产物简洁度,产物不能过于冗长,避免后续上下文超限。

沙箱批量回归评测,把提取出来的产物挂载到 Agent,在隔离沙箱环境跑测试集,包含原有回归测试集 + bad case 测试集。对比挂载该产物前后的指标:任务完成率、工具选择准确率、参数正确率、幻觉率。如果挂载产物之后,目标 bad case 得到修复,但其他大量原有正常 case 出现降级,说明产物过拟合,该产物判定质量不合格,直接丢弃。这是非常关键的一步,很多提取出来的样本只会修复个别 case,却破坏其他场景,不能上线。

人工抽样评测,自动化不能覆盖全部语义逻辑,抽样一部分产物人工审核。审核维度:规则 / 样本是否真实有效;会不会引入错误引导;是否符合业务安全规范;是否冗余啰嗦。高危业务场景,人工审核是必经环节,自动化通过也不能直接生效。

记忆法选用两套标准记忆法:提取标准筛原始轨迹,过滤残缺、bug、噪声,优先策略类失败 case,兼顾成功样本,做去重防重复;产物质量评估:格式冲突检查、沙箱回归看整体指标变化、人工抽样,重点防范过拟合修复单点而破坏全局。

用于做 Agent 自进化的数据从哪里来?怎么获得高质量的数据?怎么判断它是高质量的数据?

Agent 自进化数据源主要来自线上真实交互轨迹、人工构造测试集、沙箱仿真运行生成轨迹,三类来源。面试加分点,不能只说线上日志,要讲清楚各类数据源优缺点;重点讲高质量数据判定维度,很多人只看任务成功失败,忽略轨迹完整度、根因标签、噪声;同时说明线上原始日志绝大多数是低质量脏数据,不能直接喂给自进化流程。

第一类数据源:线上真实用户交互轨迹。线上 Agent 服务完整埋点记录每一轮完整轨迹,包含用户 query、Agent 思考过程、Skill / 工具调用入参出参、中间 To‑Do 清单、Agent 输出、用户显式反馈点赞点踩、会话结束状态。优点,贴合真实业务用户,覆盖真实世界各种边界 case;缺点,线上数据噪声极大,大量残缺会话、用户乱输入、网络异常、底层服务报错,原始日志不能直接使用。

第二类数据源:人工标注构造数据集。研发、测试人员模拟业务场景,编写各类 case,运行 Agent 生成轨迹,人工标注成败与根因。优点,数据干净,标签准确;缺点,成本高,很难覆盖海量真实用户边界场景。

第三类数据源:沙箱仿真生成轨迹。在沙箱环境,通过测试集、扰动输入,自动批量跑 Agent,生成大量仿真交互轨迹。优点,可以低成本批量生成 case,可以构造很多线上很少出现的边界场景;缺点,仿真场景和真实用户分布存在偏差。

获取高质量数据,需要一套完整的数据预处理流水线。第一步埋点规范,埋点必须采集完整全链路信息,不能只保存用户问题和最终回答,缺失思考、工具调用的轨迹无法做根因分析。第二步脏数据过滤,过滤会话中断、网络超时、Skill 底层异常报错、用户乱输入、闲聊噪声。第三步打标签,标签分为自动标签 + 人工复核标签。自动标签:依据工具返回结果、用户点赞点踩、任务是否完成标记成功失败;自动根因判断,区分是规划错误、工具选择错误、参数错误、Skill 代码 bug、知识库缺失。自动标签存在误差,高价值 bad case 需要人工复核修正标签。第四步做数据去重与多样性采样,对语义高度重复的会话做去重,避免数据集大量重复样本;兼顾正负样本比例,不能全是失败样本,需要保留一部分高质量成功交互。第五步做数据分层,区分普通数据、候选进化数据、黄金样本集。只有候选进化数据才允许送入自进化流程,黄金样本集作为回归测试集永久保存。

判断一条 Agent 交互轨迹是否高质量,有多个判定维度。首先完整性,轨迹必须完整,从用户输入到会话结束全链路,包含 Agent 思考、工具调用入参出参,中间状态;残缺中断轨迹属于低质量。其次标签正确性,成败标签、根因标签准确,能够分清问题发生在 Agent 策略层面,还是底层代码、外部依赖问题,如果根因混淆,这条数据不仅没用还会误导自进化。第三噪声程度,没有乱码、注入攻击、无效闲聊,用户意图清晰。第四业务代表性,这条轨迹代表一类真实业务场景,不是偶然随机异常。第五可复现性,在沙箱环境复现该 query,能够复现原来成功或者失败现象。如果线上偶现,沙箱无法稳定复现,这条数据质量降低,不适合用于自进化。

记忆法选用来源‑处理‑质检记忆法:数据来源:线上轨迹、人工构造、沙箱仿真;高质量数据要经过埋点、过滤、打标签、去重采样;质检看:完整性、标签准确、低噪声、业务代表性、沙箱可复现。

Agent 自进化为什么一定要在沙箱环境中做?沙箱快照与回滚机制中,快照的选择时机是怎样做的?

Agent 自进化会自动生成新的提示词、样本、Skill 配置,如果直接在线上环境运行,一旦进化产出物存在缺陷,会造成线上业务大面积出错,甚至触发高危 Skill 操作,因此自进化全流程:案例提取、效果验证、批量回归测试,都必须隔离在沙箱环境。面试加分点,不只是简单说隔离风险,区分 Agent 应用层沙箱和执行代码沙箱;重点讲快照时机,快照不是每一步都保存,要权衡存储开销与回滚能力。

沙箱环境对于 Agent 自进化的核心价值,第一风险隔离,沙箱和线上生产环境物理 / 逻辑隔离,沙箱内部 Skill 全部对接测试后端,不触碰真实业务数据,即使自进化生成错误 Skill 逻辑、错误提示词,也不会修改真实业务数据,不会影响真实用户。第二安全可控,部分 Agent 可以调用执行代码、外部工具,沙箱可以做权限限制,禁止高危操作。第三可重复可回滚,沙箱支持快照、回滚,可以对同一个测试集,快速切换 Agent 新旧版本,对比进化前后效果。第四批量回归验证,沙箱可以并发跑整套回归测试集,批量验证进化产物好坏,这是线上环境不能做的。

沙箱分为两层概念,一层是 Agent 运行环境沙箱,管控 Agent 配置:提示词、Skill 集合、Few‑Shot 样本,用来做 Agent 版本切换;另一层是代码执行沙箱,如果 Agent 支持代码执行,限制代码执行权限、网络、文件读写。Agent 自进化主要依赖前者。

快照是保存 Agent 整套完整状态,快照包含:当前 system prompt 版本、全部 Skill 元数据与配置、加载的 Few‑Shot 样本集、规划策略参数。回滚就是把 Agent 整套状态恢复到某一个快照点。快照会占用存储资源,不能每一次微小改动都生成快照,快照时机需要做策略设计。

快照的选择时机分为固定关键节点快照,不做细粒度增量快照。第一,基准基线快照,自进化流程启动之前,保存一份基线快照,对应当前线上正式 Agent 版本。所有进化出来的候选产物,都以这个基线快照做对比基准。无论后续做多少次修改,都可以一键回滚到业务基线,用来做指标对比。第二,每生成一个完整候选进化版本时生成快照。当自进化完成一轮,把提取的样本、更新的提示词组装成一个完整 Agent 版本,此时打快照。不会对中间半成品、还未完成的碎片产物打快照,减少存储消耗。第三,重大版本变更快照,当 Skill 集合发生增减、规划策略发生大改动的时候强制快照。第四,回归测试开始前快照,回归测试执行前保存状态,如果回归测试过程出现异常,直接回滚到此快照,清理本次测试改动。

不采用每一次小修改就快照的策略,频繁快照会产生海量快照文件,存储膨胀,同时版本管理混乱。快照附带元信息,记录快照生成时间、变更说明、对应的测试报告。

回滚使用场景,当某个进化候选版本在沙箱回归评测指标出现恶化,直接回滚到基线快照,丢弃该候选版本状态。只有沙箱中验证通过的快照,才允许导出配置,灰度发布到预发布环境,再逐步上线生产。沙箱快照永远不会自动同步线上,必须经过人工或者审批流程才允许导出配置。

记忆法选用隔离 + 快照时机记忆法:沙箱目的:隔离风险、可复现、批量回归,避免错误进化产物影响真实业务;快照时机:基线快照、完整候选版本完成时、重大变更前、回归测试执行前;不为中间碎片改动打快照。

沙盒的安全性你有了解吗?你知道它是怎么去做隔离的吗?

Agent 沙盒主要分为两类,一类是Agent 业务逻辑沙盒 ,隔离 Agent 配置、提示词、Skill 能力,用于自进化、版本对比、回归测试;另一类是代码执行沙盒,用于 Agent 执行生成的 Python 代码、脚本,防止恶意代码、越权操作破坏主机与业务数据。面试加分点,不要只讲 Docker 容器,要区分逻辑隔离与资源隔离,同时讲清楚攻击风险点,例如逃逸、文件读取、网络穿透,以及对应的防护手段。

沙盒安全风险来源包含几个方面,Agent 生成恶意代码读取本地敏感文件、访问内网服务;代码无限死循环耗尽 CPU 内存;代码向外发送业务敏感数据;容器逃逸拿到宿主机权限;输入提示词注入篡改沙盒内业务逻辑。Agent 自进化场景下,还会出现进化生成错误 Skill 逻辑,误调用高危业务接口篡改真实数据。

隔离手段分为逻辑隔离、进程隔离、容器隔离、操作系统级隔离多层防护。 逻辑隔离属于上层应用层隔离,不涉及底层资源。Agent 业务沙盒就属于这一层,同一套服务进程内部,为每一个评测任务维护独立上下文、独立 Skill 配置副本,Skill 全部指向测试环境后端地址,禁止访问生产接口。多个评测任务之间配置、会话状态互相隔离,不会互相污染。逻辑隔离的弱点,无法防护代码执行风险,只适合做 Agent 配置版本对比。

进程隔离,每一段待执行代码拉起独立子进程运行,设置进程资源限制,CPU 时间、最大内存,超时强制 kill 进程。优点实现简单;缺点隔离强度有限,存在进程间通信风险,不适合不可信代码。

容器 Docker 隔离是工程中最常用方案,利用 Linux 内核 Namespace 做资源隔离,Cgroups 做资源配额限制。Namespace 隔离 PID、网络、挂载点、用户 ID,让容器内部看不到宿主机进程、文件系统;Cgroups 限制最大 CPU、内存、IO,防止死循环耗尽宿主机资源。同时做容器安全加固,禁止特权模式,不挂载宿主机目录,禁用 sys‑cap 权限;网络策略做白名单,只允许访问指定测试服务,阻断内网、公网不受控访问。容器镜像为只读,执行产生数据全部放在临时存储,执行结束直接销毁容器。

更高安全等级可以使用 VM 虚拟机隔离,隔离强度最高,但是启动开销大,启动慢,并发评测场景成本很高,一般只有高风险场景使用。

除环境隔离之外,还有代码静态前置校验,在代码送入沙盒执行之前,AST 语法树解析,拦截危险 API,例如文件读写、os.system、socket 网络连接,黑名单拦截高危库导入。静态拦截不能做到 100% 防护,存在绕过手段,只能作为辅助,不能替代底层沙盒隔离。

沙盒输出也需要管控,沙盒执行返回结果做过滤,禁止透传沙盒内部路径、敏感环境变量。Agent 自进化场景下,沙盒环境所有 Skill 对接测试实例,即便沙盒被突破,也碰不到真实业务库

简易容器沙盒执行伪代码

复制代码
def run_in_sandbox(code:str):
    container_config = {
        "image":"python-sandbox:latest",
        "privileged":False,
        "network_mode":"none",
        "mem_limit":"512m",
        "cpu_period":100000,
        "cpu_quota":50000,
        "read_only":True
    }
    container = docker_client.containers.run(**container_config)
    result = container.exec_run(f"python -c {code}")
    container.remove(force=True)
    return result

记忆法选用多层隔离记忆法:逻辑隔离用于 Agent 配置版本;进程、容器 Namespace+Cgroups 做资源隔离;AST 静态检查做前置防护;网络、权限、存储加固;沙盒 Skill 必须对接测试后端。

你们项目的评测体系是怎么做的?从哪些维度去做的,评判的标准是什么?

Agent 完整评测体系分为离线自动化评测、沙箱批量回归评测、人工评测、线上观测埋点四大部分,覆盖 RAG 检索、工具调用、规划推理、生成回答全链路,不只是评测最终输出文本。面试加分点,区分传统大模型文本评测与 Agent 评测差异,Agent 评测不能只看最终文本相似度,必须把工具调用、子任务完成度、规划链路正确性纳入评测指标,很多面试只关注回答文本质量,忽略 Agent 动作链路。

评测数据集分为三套,基准回归集、bad case 测试集、边界 case 集。基准回归集沉淀业务正常场景,每次版本迭代全部跑一遍,防止版本退化;bad case 测试集收集历史线上失败案例,验证问题是否修复;边界 case 集包含模糊 query、缺参数、歧义问句、高危试探输入。数据集每条样本包含用户输入、期望标签、期望工具调用、期望中间状态、期望输出。

评测维度分为链路层指标、能力层指标、安全合规指标。 链路层指标,关注 Agent 整个执行流程是否正确。工具调用准确率,判断调用的 Skill 名称、入参是否符合预期;参数正确率,入参字段是否缺失、类型是否错误;规划完成率,To‑Do List 子任务是否全部完成,是否跳步漏步骤;终止判断准确率,判断 Agent 是否知道何时停止,避免无限循环调用工具;召回指标,如果包含 RAG 链路,统计召回率、MRR、重排相关性分数。

能力层指标,针对最终输出结果。任务完成率,是否解决用户原始诉求;回答事实准确率,是否存在幻觉;指令遵循度,是否遵守提示词格式、输出约束;回答简洁度;多轮对话一致性。

安全合规指标,是否越权调用没有权限的 Skill;是否泄露敏感信息;是否被 prompt 注入篡改行为;高危试探输入是否被拦截。

评判标准分为自动化评判、LLM‑as‑Judge 评判、人工评判。自动化评判适合结构化结果,工具调用可以做硬匹配,Skill 名称、必填参数做精确比对;数值结果可以做等值、范围比对。自动化无法处理语义类结果。

LLM‑as‑Judge 大模型裁判,传入用户 query、Agent 完整轨迹、标准答案,让裁判模型输出结构化打分,对任务完成度、幻觉、指令遵循度打分。这里需要注意裁判模型本身会有偏见,不能完全依赖裁判结果,只能作为自动化筛选,高风险样本需要人工复核。裁判 Prompt 会明确打分维度,输出分数和判断理由。

人工评判作为最终基准,抽样样本按照打分表人工打分,重点校验自动化和裁判模型存疑的 case。

一套完整评测运行流程,加载测试数据集,送入评测 Harness,在沙箱环境并发执行 Agent,采集每一条样本全链路轨迹,自动计算链路指标,调用 LLM‑as‑Judge 做结果打分,生成评测报告,对比基线版本指标变化,指标显著下降会标记版本风险。

LLM‑as‑Judge 简易裁判 prompt 片段

复制代码
你是评测裁判,根据下面信息打分。
用户问题:{{query}}
Agent完整执行轨迹:{{agent_trace}}
期望结果:{{expected}}
打分维度:任务完成度0‑5,幻觉0‑5,工具调用正确性0‑5
输出JSON,包含各项分数以及判断理由。

记忆法选用三层评测记忆法:评测四大部分:离线自动化、沙箱回归、人工评测、线上埋点;三大维度:链路层(工具、规划)、能力层(回答质量)、安全层;评判手段:硬匹配自动化、LLM 裁判、人工。

举实际例子来说说你是怎么从头到尾改进评测的?你做这个评测 Harness 的目的是为了做什么?

项目早期阶段,评测手段非常简陋,只有少量人工编写 case,研发手动跑一遍 Agent,肉眼看回答结果,没有自动化机制。每次修改提示词、Skill 描述、规划逻辑,只能人工验证修改对应的 bad case,没有回归能力,经常出现修复一个问题,把其他历史场景搞坏的退化问题。面试加分点,讲清楚真实演进过程,从手工→简单脚本→完整 Harness,讲清楚遇到的真实痛点,Harness 的核心目的不只是打分,而是尽早发现版本退化,支撑 Agent 自进化闭环

最早的第一阶段,只有十几个手工 case,没有统一框架。开发修复一个工具选择错误的 bad case,复现验证通过之后就上线。上线之后收到用户反馈,另一个原本正常的查询场景出现异常,因为修改 Skill 描述之后干扰了其他相近意图的判断。这时候意识到缺少回归能力,只测目标 bad case 远远不够。

第二阶段,写简单 Python 脚本,加载一批测试样本,循环调用 Agent 服务,把输入输出存到文件,人工打开文件逐条看结果。只能拿到输入输出文本,拿不到 Agent 中间思考、To‑Do 状态、工具调用参数,只能评判最终回答,无法判断是规划错还是工具参数错。很多 case 回答看着差不多,但内部工具调用逻辑已经出错,问题被掩盖。同时没有基线对比,无法直观看到新版本对比旧版本变好还是变差。

第三阶段,开始搭建评测 Harness 雏形。改造 Agent 埋点,每一次评测运行输出完整结构化轨迹,包含思考过程、Skill 调用、入参出参、To‑Do 状态、中间所有变量。数据集增加字段,不仅仅存期望回答,还增加期望工具调用、期望子任务。加入自动化硬匹配规则,校验 Skill 名称、必填参数。引入 LLM‑as‑Judge 做语义打分。保存基线版本运行报告,新版本跑完自动对比指标变化,标记指标下降的 case。

第四阶段完善 Harness,对接沙箱环境,所有评测样本全部隔离沙箱运行,不会触碰真实业务数据;支持数据集分类:回归集、bad case 集、边界 case 集;支持分组跑评测,比如只跑知识库相关 case,只跑工具调用 case;生成可视化报告,列出退化 case 列表,导出完整 Agent 轨迹,方便研发定位根因;为 Agent 自进化做支撑,自进化产出的每一个候选产物,自动送入 Harness 批量评测,指标不达标直接丢弃候选产物。

举一个真实改进案例:业务出现一类问题,用户查询报销统计,Agent 选错 Skill,调用了员工信息查询。首先把这个 bad case 加入测试集。早期简陋脚本只能看到最终回答错误;Harness 可以捕获完整轨迹,看到 Agent 规划阶段选错 Skill,入参传递错误。当修改 Skill 描述之后,重新跑完整回归集,Harness 不仅校验这个 bug 是否修复,同时自动校验全部历史回归样本,发现有 3 个 FAQ 场景出现工具误触发,指标下降,直接阻止该配置直接发布,避免线上退化。

评测 Harness 的核心目的,第一解决手工测试覆盖不足、回归缺失的痛点,每次改动自动批量验证,防止版本退化;第二捕获完整 Agent 链路信息,不仅仅看最终文本,定位是规划、工具、提示词哪一层出错;第三标准化评测流程,把评测从研发手动操作变成自动化流水线环节;第四支撑 Agent 自进化,自进化生成大量候选配置,人工不可能全部核验,依靠 Harness 在沙箱批量筛选候选产物;第五沉淀测试数据集,持续积累 bad case,形成资产。

伪代码,Harness 核心调度逻辑

复制代码
class EvalHarness:
    def __init__(self,agent_sandbox,test_dataset):
        self.sandbox = agent_sandbox
        self.dataset = test_dataset
        self.baseline_report = None

    def run(self):
        result_list = []
        for sample in self.dataset:
            trace = self.sandbox.run_agent(sample["query"])
            metric = self.calc_metric(trace,sample["expect"])
            result_list.append({"trace":trace,"metric":metric})
        report = self.gen_report(result_list)
        if self.baseline_report:
            report.compare_with_baseline(self.baseline_report)
        return report

记忆法选用演进四阶段记忆法:手工肉眼评测→简单脚本输出文本→采集全链路轨迹 + 自动化 + LLM 裁判→对接沙箱支撑自进化;Harness 目的:防版本退化、定位链路问题、自动化流水线、支撑自进化候选筛选。

你构建的评测体系是如何和你们业务内的实际场景去应用的?

评测体系不是独立离线工具,深度嵌入研发迭代、版本发布、Agent 自进化、线上问题复盘全业务流程。面试加分点,不要只说跑测试集,要讲清楚在研发开发、CI 流水线、自进化闭环、线上故障回溯这几个真实业务环节如何落地,同时说明评测体系的边界,自动化不能替代人工。

在日常研发迭代场景,研发修改提示词、Skill 元数据、规划策略逻辑之后,本地或者测试环境启动评测 Harness,运行对应业务域测试子集。比如修改报销相关 Skill 描述,就跑报销相关全部回归样本。如果评测报告出现任务完成率下降、工具调用准确率下跌,研发优先定位退化 case,查看完整 Agent 轨迹,修复问题之后再提交代码。不会等到上线前才一次性全量跑,把问题前置在开发阶段。

CI 持续集成流水线接入评测 Harness。代码合并到主分支时,流水线自动拉起沙箱环境,执行完整基准回归测试集。如果关键指标发生显著退化,流水线阻断合并,研发需要分析评测报告里面标记的退化样本。这里不会跑全部几十万条 case,全量耗时太长,CI 跑核心回归集;完整全量评测在预发布环境定时执行。CI 只做门禁,拦截明显退化,兼顾迭代效率。

版本发布预发布环节,部署新版本 Agent 到预发布环境,执行完整全量评测集,包含 bad case 全集、边界 case。对比基线版本完整指标报告。只有核心指标没有明显下降,并且新增 bad case 全部修复,才允许灰度放量到生产。预发布阶段也会做小流量真实用户影子评测,线上真实 query 复制一份同时送入旧版本和新版本 Agent,对比两边输出,不影响用户,用来发现离线测试集没有覆盖的真实场景。

Agent 自进化业务闭环中,评测体系是核心校验关卡。自进化流程产出的候选产物,例如新 Few‑Shot 样本、更新提示词片段、优化 Skill 描述,全部送入沙箱 Harness 执行批量评测。一方面验证对应的 bad case 是否被修复;另一方面跑完整回归集,校验是否带来其他场景退化。如果指标不达标,直接丢弃该候选产物;评测通过之后,再送入人工抽检,人工审核通过之后才允许导出配置进入候选版本池,不会自动上线生产。

线上问题复盘流程,线上收到用户反馈 bad case,首先把这条 query 加入测试数据集。复现完整 Agent 交互轨迹,使用 Harness 单独执行该样本,复现故障现象,定位根因。修复完成之后,该 case 永久进入回归集,后续任何版本迭代都会自动校验该场景,防止问题重复出现。

同时评测体系存在边界,自动化评测不能 100% 替代人工。LLM‑as‑Judge 裁判存在误判,业务高风险场景,自动化通过之后依旧需要人工抽样复核。离线测试集不可能覆盖全部真实用户无穷无尽的输入,线上依旧保留埋点观测,作为离线评测的补充。离线评测看可控测试样本,线上埋点看真实用户分布下的实际表现,两者结合。

记忆法选用业务流程嵌入记忆法:开发阶段本地跑子集;CI 流水线做门禁拦截退化;预发布全量评测 + 影子流量;自进化的校验关卡;线上 bad case 回流扩充数据集;离线评测配合线上埋点互相补充。

你们的 Harness 层现在是怎么去构建的?

评测 Harness 是一套完整评测编排框架,整体分为五层架构:数据集管理层、沙箱调度层、Agent 执行层、指标计算层、报告输出层。面试加分点,讲清楚每层职责,模块解耦设计,Harness 不耦合具体 Agent 业务逻辑,可以支持 RAG、Tool‑Agent、自进化候选配置等多种对象评测,同时讲并发、存储、可扩展性设计,给出模块伪代码。

数据集管理层,负责管理全部评测样本集。支持多种数据集:基准回归集、bad case 集、边界测试集。样本存储可以使用 JSON 文件或者数据库,每条样本字段包含唯一 id、用户 query、标签分类、期望输出、期望工具调用列表、期望 To‑Do 子任务、业务域标签、备注。支持数据集的分组筛选,可以按业务域、标签过滤样本;支持新增、导入线上回流 bad case;支持版本管理,数据集本身也做版本,数据集变更也会记录,避免数据集改动带来评测结果不可对比。数据集层对外提供统一读取接口,上层 Harness 不需要关心底层存储。

沙箱调度层,Harness 不直接调用生产 Agent,统一对接 Agent 沙箱。支持多隔离沙箱实例并发运行评测任务,每个评测任务实例拥有独立 Agent 配置副本,提示词、Skill 集合、Few‑Shot 互相隔离。沙箱调度层负责管理沙箱实例生命周期,控制并发数,防止并发过高压垮服务;任务执行完成之后销毁沙箱实例,清理状态。支持加载不同 Agent 配置版本,基线版本、待评测新版本、自进化生成候选配置,可以在同一套数据集上批量跑多版本做横向对比。

Agent 执行层,接收沙箱返回完整结构化 Agent 轨迹。轨迹是结构化 JSON,不仅仅保存问答文本,包含用户输入、每一轮思考、To‑Do List 状态、Skill 调用入参出参、中间 RAG 召回片段、终止原因、异常报错。执行层负责轨迹持久化存储,每一条样本的完整轨迹都会落库,后续定位退化 case 可以直接查看完整链路,不需要复现。

指标计算层分为三类计算组件。第一类硬规则校验组件,自动化规则,校验工具调用名称、必填参数、格式、字段类型,做精确匹配;第二类 LLM‑as‑Judge 裁判组件,调用裁判大模型,传入 query、完整轨迹、样本期望,输出结构化打分;第三类聚合统计组件,汇总全部样本结果,计算任务完成率、工具调用准确率、规划完成率等聚合指标。指标层解耦,新增评测指标只需要新增计算组件,不改动主调度流程。

报告输出层,把单条样本结果、聚合指标、版本对比结果生成评测报告。报告包含聚合指标总览、退化 case 列表、每条 case 跳转完整轨迹详情。支持输出 JSON 结构化报告,可供程序解析,用于 CI 流水线判断门禁条件;同时支持可视化网页报告,方便研发查看分析。支持新旧版本对比,高亮哪些指标上升,哪些指标下降,列出退化样本 id。

整体工作流程,Harness 接收评测任务,指定数据集名称、待评测 Agent 配置、基线 Agent 配置;数据集管理层加载对应样本;沙箱调度层拉起一批隔离沙箱;并发执行样本,采集全量轨迹;指标计算层执行硬规则校验、LLM 裁判打分;聚合指标;生成报告,对比基线;输出结构化结果供 CI 或者自进化流程消费。

Harness 架构简易伪代码

复制代码
class EvalHarnessFramework:
    def __init__(self,dataset_manager,sandbox_scheduler,metric_calc,report_generator):
        self.dataset_mgr = dataset_manager
        self.sandbox_scheduler = sandbox_scheduler
        self.metric_calculator = metric_calc
        self.report_gen = report_generator

    def run_eval_task(self,dataset_id,test_agent_conf,baseline_agent_conf=None):
        samples = self.dataset_mgr.load_dataset(dataset_id)
        test_sandboxes = self.sandbox_scheduler.spawn(test_agent_conf,count=8)
        test_trace_list = self._run_samples(samples,test_sandboxes)
        test_metrics = self.metric_calculator.calc_all(test_trace_list,samples)
        baseline_metrics = None
        if baseline_agent_conf:
            base_sandboxes = self.sandbox_scheduler.spawn(baseline_agent_conf,count=8)
            base_trace_list = self._run_samples(samples,base_sandboxes)
            baseline_metrics = self.metric_calculator.calc_all(base_trace_list,samples)
        report = self.report_gen.generate(test_metrics,baseline_metrics)
        return report

记忆法选用五层架构记忆法:数据集管理层、沙箱调度层、Agent 执行轨迹采集层、指标计算层、报告输出层;核心设计:解耦业务逻辑,采集完整结构化轨迹,支持多版本横向对比,输出机器可读报告用于 CI 与自进化。

怎么评估 Agent 效果?没有用户反馈时,怎么做有效抽检?

Agent 效果评估是全链路评估,不能只看最终回答文本,需要覆盖输入解析、意图识别、规划推理、Skill / 工具调用、参数组装、工具执行、结果整理输出完整链路。完整评估手段分为离线自动化评测 Harness、沙箱批量回归、LLM‑as‑Judge 裁判评测、人工抽检、线上埋点统计五大块。面试加分点,重点阐述无用户显式反馈场景下的抽检方案,很多面试只讲有用户点赞点踩的情况,忽略绝大多数线上会话没有任何用户反馈的现实业务场景。

有用户反馈时,可以直接利用点赞、点踩、会话放弃、用户纠错文本作为正负样本标签。但线上真实环境绝大多数会话用户不会给出任何反馈,不能只依靠显式反馈做质量发现。

离线 Harness 评测,使用回归数据集、bad case 集、边界 case 在沙箱批量运行,输出工具调用准确率、规划完成率、任务完成率、幻觉率等聚合指标,作为版本迭代基础评估。但离线测试集无法覆盖全部真实用户输入,不能完全等价线上真实表现。

LLM‑as‑Judge 裁判评测,既可以用于离线数据集,也可以用于线上采样会话。裁判模型读取完整 Agent 交互轨迹,包含用户 query、思考过程、工具调用入参出参、最终回答,输出结构化打分,维度包含任务完成度、工具调用正确性、幻觉、指令遵循度。裁判存在固有偏见,只能做初筛,裁判判定低分的会话送入人工复核,高分样本也需要抽样验证裁判准确率。

无用户反馈的有效抽检分为几种策略。第一种分层随机抽样,按照业务域、会话复杂度分层抽样,简单问答、多轮复杂任务、多工具调用任务按比例抽取,不做完全随机。完全随机会导致大部分抽到简单会话,复杂问题占比很低,漏掉高风险场景。会统计会话特征:工具调用次数、是否多轮对话、是否触发 RAG、是否执行写操作 Skill,按照特征分层,保证复杂任务有足够采样占比。

第二种异常信号导向抽检,不需要用户反馈,依靠系统埋点信号筛选可疑会话优先抽检。信号包括 Agent 触发工具重试次数过多、Agent 循环调用工具无法终止、工具返回报错、回答 "无法完成任务"、To‑Do List 大量任务未完成、检索返回空结果、参数校验失败。出现这类信号,无论用户是否反馈,都标记为高可疑样本,优先送入人工抽检队列。这类样本里面 bad case 密度远高于普通随机抽样,可以大幅提升抽检效率。

第三种影子流量对比抽检,线上真实 query 复制一份,同时送入基线版本 Agent 和待观测新版本 Agent,两份结果做对比。当新旧版本输出的工具调用、最终回答差异较大时,标记该会话进入抽检队列;输出一致的会话降低抽样权重。不需要用户参与,就能发现版本变更带来的行为变化。

第四种语义聚类抽检,对线上海量会话 query 做向量化聚类,每个聚类簇抽取少量样本。可以避免大量抽检高度重复的 query,覆盖更多不同语义场景,提升样本多样性。

人工抽检拿到样本之后,评审人员查看完整结构化 Agent 轨迹,不只看最终回答,定位问题出在哪一层:意图识别、规划、工具选择、参数、RAG、Skill 内部逻辑。抽检完成后,bad case 回流到评测数据集,进入后续回归集。

伪代码:异常信号筛选抽检候选

复制代码
def filter_suspect_traces(trace_list):
    suspect = []
    for trace in trace_list:
        if trace["tool_retry_count"] >=2:
            suspect.append(trace)
        elif trace["agent_loop_detected"] is True:
            suspect.append(trace)
        elif trace["tool_error_count"]>0:
            suspect.append(trace)
        elif trace["todo_unfinished"]>0:
            suspect.append(trace)
    return suspect

记忆法选用分层 + 异常信号记忆法:Agent 评估覆盖全链路;无用户反馈抽检手段:分层随机抽样、异常信号优先筛选、影子流量对比、语义聚类;优先抓取有异常埋点信号的会话提升 bad case 命中率。

出现 Badcase 怎么快速定位环节?如何判断该给哪个 Agent 做 SFT 优化?Prompt 调优 "修好一类、坏了另一类" 怎么解决?

当出现一条 Agent bad case,不能直接修改提示词或者做微调,首要工作是依据完整结构化交互轨迹逐层定位根因。面试加分点,给出清晰的故障分层定位流程,区分哪些问题适合 SFT,哪些不适合;针对 Prompt 调优修复一类 case 但是污染其他场景的退化问题,给出工程化解决方案,而不是只靠反复修改 prompt 文本试错。

bad case 快速定位环节,拿到完整 Agent 轨迹,包含用户 query、意图识别结果、思考过程、To‑Do 待办、Skill 调用入参出参、RAG 召回内容、工具返回、最终回答。按照固定顺序逐层排查。 第一层,确认是不是外部依赖问题:Skill 工具本身返回错误数据、数据库查询异常、RAG 召回文档错误。如果是工具 / 知识库本身问题,属于外部依赖,不需要改动 Agent 模型与提示词,优先修复底层服务。 第二层,判断规划层问题:Agent 是否理解用户意图,是否选错 Skill,是否漏子任务,是否跳步,To‑Do List 规划不合理。现象是工具本身返回正确数据,但 Agent 选错工具、调用顺序错误。 第三层,入参解析问题:Skill 选对,但是参数缺失、参数错误、参数格式不对,属于参数解析能力问题。 第四层,结果整理生成问题:工具返回数据完全正确,但 Agent 汇总输出的时候出现幻觉、总结错误,属于后处理生成问题。 第五层,提示词约束问题:模型本身能力足够,但缺少约束规则、Few‑Shot 样例不足。 第六层,基座模型固有能力缺陷:多次优化提示词、补充样例之后同类问题依旧高频出现,属于模型本身推理、函数调用能力不足。

如何判断是否适合做 SFT,以及 SFT 作用对象。SFT 监督微调适合修复模型本身推理行为缺陷 ,例如高频规划错误、高频工具参数解析错误,经过充分的 Prompt、Few‑Shot 优化之后依然反复出现同一类错误,并且可以收集到一批高质量轨迹样本,才考虑 SFT。 如果问题是 Skill 业务 bug、RAG 召回差、提示词规则缺失,不要做 SFT,SFT 无法修复外部依赖缺陷,还会引入不可控副作用。 区分微调对象:如果是主 Agent 规划、工具选择行为持续出错,对主 Agent 做 SFT;如果是子 Agent 负责的子任务反复出错,则针对对应子 Agent 构造 SFT 数据集;不要盲目把全部轨迹一股脑喂给基座模型。SFT 数据集需要严格过滤脏样本,只能使用高质量正确轨迹,混入错误轨迹会恶化整体能力。

Prompt 调优修好一类 case,但是弄坏另一类 case,这是 Agent 开发非常典型的过拟合现象。根源在于新增约束、新增 Few‑Shot 样本之后,上下文信息发生变化,干扰模型在其他场景的行为。传统做法是反复修改 prompt 试错,效率极低。工程解决手段分为四点。 第一,必须拥有完整回归评测集与 Harness 沙箱回归能力。每一次修改提示词、新增 Few‑Shot,不能只验证目标 bad case,必须在沙箱运行全套回归测试集。如果出现其他 case 指标下降,该 prompt 变更不能直接采纳。这是最核心防线,防止局部修复带来全局退化。 第二,区分全局约束与局部约束。不要把所有样例全部塞到全局 system prompt。通用规则保留在全局;场景化 Few‑Shot、特殊约束改为检索式 Few‑Shot,构建样例向量库,当用户 query 语义匹配对应场景,才动态把这部分样例注入上下文,不匹配的场景不加载,避免无关样例干扰其他任务。 第三,约束做模块化拆分。system prompt 拆分为多个模块:通用基础规则、工具描述模块、可选业务模块。按需拼装 prompt,而不是全部文本堆砌在一起。 第四,Prompt 版本管理与 A/B 灰度。同一个业务维护多个候选 prompt 版本,沙箱评测通过之后,线上小流量灰度对比,观测线上真实指标,确认没有退化再全量发布。

bad case 定位简易排查流程伪代码

复制代码
def locate_badcase_rootcause(trace):
    if trace["tool_raw_error"]:
        return "skill_external_bug"
    if trace["skill_wrong_choose"]:
        return "plan_error"
    if trace["param_validate_fail"]:
        return "param_parse_error"
    if trace["tool_return_ok"] and trace["final_answer_wrong"]:
        return "summary_gen_error"
    return "prompt_or_model_cap"

记忆法选用逐层定位记忆法:bad case 定位顺序:外部依赖→规划→参数解析→结果生成→prompt→模型能力;SFT 仅用于模型固有行为缺陷,排除底层 bug;prompt 局部修复引发退化:依靠回归 Harness、检索式 Few‑Shot、模块化 prompt、灰度发布。

开发 Agent 时踩过哪些坑?

Agent 开发的坑分布在数据集、提示词、规划逻辑、工具调用、沙箱评测、上线运行、自进化多个环节,很多坑在 demo 阶段不会暴露,上线真实业务流量之后才集中显现。面试加分点,不只是罗列现象,要讲清楚坑的现象、产生原因、落地解决手段,贴合工程真实场景。

第一类坑,Demo 效果很好,上线真实业务效果大幅下滑。demo 测试样本都是研发精心构造的清晰 query;真实用户输入大量口语化、模糊、信息缺失、歧义问句。模型在 demo 表现优秀,真实流量频繁出现选错工具、参数不全。解决手段,早期就要引入真实线上 bad case 构建测试集,不要只使用人工理想 case;增加查询改写、缺失参数反问机制;分层评测,不要以 demo 表现作为上线标准。

第二类坑,工具 Function‑call 描述写的越多越详细,反而引发误调用。工具描述堆砌大量业务细节,上下文内容膨胀,模型容易混淆相近 Skill,出现乱调用。解决手段,工具描述精炼,只保留核心用途、入参含义;Skill 做语义聚类,意图相近的 Skill 做区分描述;评测 Harness 统计工具误调用率,持续迭代描述文本。

第三类坑,Agent 无限循环工具调用,不会终止任务。大模型反复调用同一个工具,拿到结果之后依旧不输出答案,持续循环。原因包括缺少终止判断约束、工具返回结果格式模型无法理解、没有最大轮次限制。解决手段,增加 To‑Do List 状态管理,全部任务标记完成才允许结束;设置最大 Agent 迭代轮次,框架层面强制熔断;prompt 增加终止条件;轨迹埋点检测循环特征。

第四类坑,Few‑Shot 样例乱堆,修复部分 case,带来大范围退化。把大量 bad case 样例全部塞进 system prompt,上下文 token 暴涨,无关样例互相干扰,其他业务场景能力下降。解决手段,改用检索式 Few‑Shot,只有语义匹配才动态注入样例;回归 Harness 每次变更跑全套回归集,防止过拟合。

第五类坑,评测只看最终回答文本,忽略中间链路。很多 bad case 回答表面看起来没问题,但内部工具调用错误,参数错误,只是恰好输出看上去合理的文本,线上流量放大之后集中爆发故障。解决手段,评测 Harness 采集完整 Agent 轨迹,把工具调用、规划状态纳入硬性评测指标,不只评估输出文本。

第六类坑,自进化直接把自动生成产物上线。自进化产出优化后的提示词、样本,仅仅修复了当前 bad case,却对其他场景造成破坏,如果没有沙箱回归校验直接上线,引发线上大面积故障。解决手段,自进化所有候选产物必须经过沙箱 Harness 回归评测,指标合格再人工抽检,禁止自动生效到生产。

第七类坑,沙盒逻辑隔离当成绝对安全,忽略代码执行风险。业务 Agent 沙盒只做了 Agent 配置逻辑隔离,当 Agent 支持代码执行,没有做容器资源隔离、AST 前置校验,会存在代码逃逸、耗尽资源风险。解决手段,不可信代码必须使用容器沙盒做 Namespace+Cgroups 隔离,静态 AST 校验作为辅助防护。

第八类坑,过度依赖 SFT 解决一切问题。遇到 bad case 第一反应做微调,很多问题根源是 Skill bug、prompt 缺失、RAG 召回差,SFT 不仅无法修复,还污染模型行为。解决手段,先做 bad case 根因分层定位,区分底层问题与模型行为问题,严格控制 SFT 使用场景。

第九类坑,多轮对话上下文无限累积,token 持续膨胀,老历史信息干扰当前轮推理。会话轮次一多,模型被旧对话带偏。解决手段,实现滚动窗口、久远历史摘要压缩,过滤无效工具报错日志,裁剪上下文。

记忆法选用demo‑上线差异记忆法:主要坑点:demo 与真实流量差距、工具描述冗余误调用、Agent 循环不终止、Few‑Shot 过拟合、只评测输出文本、自进化无校验、沙盒安全不足、滥用 SFT、上下文无限膨胀;每个坑配套工程解决手段。

项目经历里那个 Hello Agent 智能体的架构,你为什么会把它转化为 Go 语言?你是出于什么考虑做这样的选型?使用 Go 语言重写其中遇到什么问题吗?

Hello Agent 最初原型版本使用 Python 开发完成,完整实现 Agent 核心链路:意图识别、规划、To‑Do List 维护、Skill 调度、RAG 对接、评测 Harness 原型。Python 开发速度快,适合快速验证算法逻辑、业务流程,快速跑通原型,验证 Agent 业务可行性。当业务开始承接线上真实流量,并发 QPS 上涨,同时需要和内部 Go 技术栈服务做深度集成,因此启动 Go 语言重写。面试加分点,客观对比 Python 与 Go 的优劣势,不片面吹捧某一门语言,同时讲重写过程真实遇到的困难,不要只讲收益,回避迁移代价。

选型转为 Go 语言主要出于几方面考量。 第一线上性能与并发能力。Python GIL 锁限制 CPU 密集场景,高并发下协程调度开销大;Agent 每一条请求内部包含多轮 LLM 调用、Skill 调用、IO 等待,Go goroutine 轻量级协程对 IO 密集型 Agent 服务非常友好,内存占用更低,相同硬件资源可以支撑更高并发。原型阶段 QPS 很低,Python 问题不明显;线上流量上涨之后,Python 版本内存上涨明显,GC 卡顿,服务扩容成本高。

第二内部技术栈统一。公司后端主体服务全部是 Go 开发,鉴权、网关、配置中心、注册发现、日志链路追踪、监控告警组件全部提供 Go SDK,Python 版本需要做大量适配封装,版本兼容维护成本高。Go 重写之后可以直接复用公司现成中间件 SDK,链路追踪、埋点监控可以无缝对齐,运维体系统一。

第三部署与运维交付。Go 编译为单一二进制文件,部署不需要依赖 Python 环境、繁多依赖包,避免环境依赖冲突;容器镜像体积更小,启动速度快,适合 K8s 大规模弹性扩缩容。Python 经常出现依赖包版本冲突,测试环境和生产环境依赖不一致引发诡异 bug。

第四稳定性与故障可控。Go 内置丰富的并发原语,goroutine、channel,可以很方便控制每一条 Agent 请求的子协程生命周期,设置 context 上下文超时,协程泄漏更容易排查;Python 异步模型开发复杂,协程泄漏、超时控制处理繁琐。Agent 任务存在多轮外部调用,context 传递超时、取消能力非常关键。

并不是 Go 全方面优于 Python,Python 依旧适合算法原型、Harness 评测脚本、数据分析工作,重写只是把线上运行的 Agent 服务迁移,评测脚本、bad case 分析工具依旧保留 Python。

Go 重写过程遇到的问题。 第一,Agent 业务逻辑大量字符串处理、复杂 Prompt 模板拼接、JSON 结构化解析,Python 动态特性开发效率高;Go 是静态强类型语言,结构体定义繁琐,JSON 序列化反序列化字段需要精细处理,模型返回的非标准 JSON 容错解析需要大量适配代码。大模型输出 JSON 经常存在逗号缺失、多余换行,在 Python 中可以用宽松库容错,Go 需要手动实现容错解析逻辑,工作量比原型阶段大很多。

第二,业务原型阶段大量使用动态字典存储 Agent 轨迹、Skill 元数据,Go 需要提前定义完整结构体,业务字段迭代变更的时候,结构体需要同步修改,迭代速度相比 Python 原型变慢。需要做好 proto 或者结构体版本兼容处理。

第三,LLM 客户端 SDK 适配。很多大模型 SDK 对 Python 支持完善,Go 生态库相对少,需要自己封装 OpenAI 兼容接口客户端,处理流式返回、错误重试、超时逻辑。

第四,原型业务逻辑迁移过程中逻辑一致性对齐风险。Python 原型业务逻辑迁移 Go,很容易出现逻辑细节不一致,例如 prompt 拼接换行、参数默认值、边界判断条件,导致 Go 版本 Agent 行为和旧 Python 版本不一致。解决手段,使用影子流量对比,线上真实 query 同时打给旧 Python 版本和新 Go 版本,对比两边 Agent 完整轨迹,工具调用、中间状态、输出结果做比对,发现逻辑差异逐个修复,配合评测 Harness 全套回归集做一致性校验。

第五,开发迭代效率下降。原型阶段 Python 快速改代码验证想法;Go 需要编译,结构体改动涉及多处修改,快速试错成本变高。团队策略保持原型验证依旧使用 Python,逻辑确认稳定之后再同步到 Go 线上服务。

记忆法选用原型‑生产分离记忆法:Python 用于快速原型验证;Go 迁移原因:并发性能、内部技术栈统一、运维部署优势、context 超时控制;重写遇到问题:静态类型 JSON 解析繁琐、结构体迭代、缺少 SDK、逻辑对齐风险、迭代速度降低,依靠影子流量 + Harness 回归保证一致性。

说一下模块和模块之间的依赖关系?这些模块是怎么做的?

Hello Agent 整体架构分为七大核心模块,分别是接入网关模块、会话管理模块、意图解析模块、Agent 规划调度模块、Skill 执行调度模块、RAG 检索模块、观测与埋点模块。模块之间做依赖解耦,上层模块依赖下层抽象接口,不直接依赖下层具体实现,便于单元测试、替换实现。面试加分点,梳理清楚依赖方向,禁止循环依赖,说明模块之间通信方式,同时说明每个模块的职责与实现要点。

依赖方向自上而下:接入网关模块作为最上层入口;网关调用会话管理模块;会话管理模块调用意图解析模块、Agent 规划调度模块;Agent 规划调度模块依赖 Skill 执行调度模块、RAG 检索模块;所有模块都依赖底层公共观测埋点组件。模块之间不存在循环依赖,下层模块不能反向 import 上层模块。

接入网关模块,接收外部 HTTP/gRPC 请求,负责鉴权、限流、参数校验,请求转发给会话管理。不处理 Agent 业务逻辑。实现上基于 Go 标准库或者 gRPC 框架,做请求预处理,设置全局 context,携带 traceId 透传给下游,请求失败统一返回标准化错误码。网关只依赖公共埋点组件,不依赖其他业务模块。

会话管理模块,管理多轮会话上下文,维护会话历史消息、To‑Do List 状态、会话配置。负责上下文窗口裁剪、历史摘要压缩,组装送入大模型的完整消息列表。当新用户请求到来,加载会话历史,把组装好的上下文对象传给意图解析模块与 Agent 规划调度模块。会话管理模块依赖公共埋点;不感知 Skill、RAG 内部实现,只调用抽象接口。会话数据存储可以对接 Redis,会话状态做序列化,支持会话持久化。

意图解析模块,接收会话管理传入用户 query 与上下文,执行并行意图识别,输出识别出来的意图列表。内部可以封装大模型调用或者小模型推理。该模块只依赖公共埋点组件,输出结构化意图结果给规划调度模块,不调用 Skill、RAG。

Agent 规划调度模块,是 Agent 的核心大脑。接收会话上下文、意图识别结果,完成任务规划,维护 To‑Do List 任务清单,决策下一步动作:调用 Skill、调用 RAG 检索、直接生成回答结束会话。控制 Agent 最大迭代轮次,做熔断。规划模块依赖 Skill 执行调度抽象接口、RAG 检索抽象接口、埋点组件。规划模块只调用抽象接口,不关心 Skill 底层是 HTTP 调用还是本地函数实现。当规划判定需要调用工具,下发调用指令给 Skill 执行调度;需要检索,下发指令给 RAG 模块;可以结束任务则整理结果返回会话管理。

Skill 执行调度模块,统一管理全部 Skill 元数据,负责 Skill 路由、入参 schema 校验、权限校验、执行 Skill,捕获执行异常,标准化返回结果。Skill 分为本地内联 Skill、远程 MCP Skill。调度层定义 Skill 抽象接口,所有 Skill 实现该接口。规划模块依赖这个抽象接口,而不是各个具体 Skill 实现。新增 Skill 不需要改动规划模块代码,只需要新增 Skill 实现并注册到调度器。Skill 执行调度依赖埋点组件,不反向依赖规划模块。

RAG 检索模块,接收规划调度下发检索请求,执行 query 改写、多路召回、重排,返回参考文档片段。对外暴露统一检索接口,规划模块调用该接口,不关心内部是 ES 还是向量库实现。依赖埋点组件。

LLM 的底层原理有没有了解?输入给模型的是什么?

大语言模型 LLM 本质是基于 Transformer 解码器堆叠的自回归生成模型,核心目标是根据前文序列,预测下一个最可能出现的 token,不断迭代输出完整文本。面试加分点,区分模型接收的不是原始字符串,而是 token id 序列,讲清楚完整输入链路:原始文本→分词 tokenizer→token id→位置编码→嵌入向量,很多面试容易混淆字符串、token、向量三者。

底层主体结构由多层 Decoder 解码器堆叠而成,每一层内部包含掩码自注意力 Masked‑Self‑Attention、前馈神经网络 FFN、层归一化 LayerNorm、残差连接。掩码机制保证预测第 i 个 token 时,看不到 i 位置之后未来的 token,防止信息泄露。自回归生成模式,每一步输入历史全部 token 序列,输出下一个 token 的概率分布,采样得到新 token 追加到输入序列,循环直到遇到结束符 EOS。

输入给到模型的并不是我们肉眼看到的字符串文本,完整处理流程分为多步。 第一步原始文本经过 Tokenizer 分词器处理,把中文、英文、符号切分成一个个 token 子词单元。中文经常一个汉字就是一个 token,英文会拆分子词。每个 token 维护一张词表映射表,每一个 token 对应唯一整数 token_id。特殊 token 也在这里处理,比如 BOS 句子开始、EOS 句子结束、PAD 填充 token,对话场景会加入角色标记,区分 system、user、assistant。

第二步把 token_id 列表查表得到词嵌入 embedding 向量,每个 token_id 映射为固定维度的向量,例如 4096 维。词嵌入向量负责表达 token 本身语义信息。

第三步加入位置编码 Positional Encoding。Transformer 本身没有时序概念,向量本身不携带位置信息,必须叠加位置编码,区分同一个 token 出现在句子不同位置。位置编码可以是正弦余弦计算出来,也可以是可学习训练得到的位置 embedding。词嵌入向量与位置编码向量逐元素相加,得到输入向量。

第四步向量送入多层 Decoder 网络,经过掩码自注意力、FFN 层层计算,最后输出层映射回词表维度,得到下一个 token 的概率分布。

对话、Agent 场景下输入会组装完整 prompt 上下文,包含 system 提示词、用户 query、历史多轮对话、工具描述、Few‑Shot 样例,全部拼接成字符串,经过 Tokenizer 转成 token id 序列送入模型。输入长度受上下文窗口限制,超过最大上下文窗口需要做截断或者摘要压缩。

简易伪代码描述输入处理链路

复制代码
raw_text = "请帮我查询报销数据"
token_ids = tokenizer.encode(raw_text)
token_embeds = embedding_layer(token_ids)
pos_embeds = position_encoding(len(token_ids))
model_input_embeds = token_embeds + pos_embeds
output_logits = decoder_stack(model_input_embeds)
next_token_prob = softmax(output_logits[-1])

记忆法选用输入链路记忆法:原始字符串→Tokenizer 切分 token 得到 token_id→词嵌入向量→叠加位置编码→送入 Decoder;模型真正接收的是向量,不是字符串。

用于做 Agent 自进化的数据从哪来的?怎么获得高质量的数据的?怎么判断它是一个高质量的数据的?

Agent 自进化的数据来源分为三大类:线上真实用户交互轨迹、人工标注构造数据集、沙箱仿真批量生成轨迹。面试加分点,线上原始日志绝大多数属于脏数据,不能直接喂入自进化;明确高质量数据的判定标准,区分哪些问题属于 Agent 策略问题,哪些属于底层 bug,底层 bug 对应的轨迹不能用于自进化。

第一类线上真实交互轨迹。线上 Agent 服务完整埋点存储每一条会话全链路轨迹,包含用户 query、Agent 思考推理过程、To‑Do List 变化、Skill 工具调用入参出参、RAG 召回片段、Agent 输出、用户点赞点踩反馈、会话终止状态。优点,覆盖真实用户各种边界场景,是最有价值数据源;缺点噪声巨大,大量会话中断、网络异常、用户乱输入、Skill 底层服务报错。

第二类人工标注构造数据集。研发、测试人员模拟业务场景,构造正常 case、边界 case、失败 case,运行 Agent 产生完整轨迹,人工标注成败标签与根因。优点标签准确、数据干净;缺点人力成本高,很难覆盖真实用户五花八门的输入。

第三类沙箱仿真生成轨迹。在隔离沙箱环境,基于已有测试集做扰动改写,批量跑 Agent 生成大量仿真交互轨迹。优点可以低成本批量构造罕见边界场景;缺点仿真分布和真实用户存在偏差,不能完全替代真实线上数据。

获取高质量数据需要完整的数据处理流水线。首先要有完备埋点,不能只保存用户问题和最终回答,缺少思考、工具调用、中间状态的轨迹无法做根因分析。其次执行脏数据过滤,过滤会话中断、网络超时、Skill 底层代码 bug、闲聊乱输入、注入攻击样本。Skill 本身业务 bug 造成失败,不属于 Agent 策略问题,直接丢弃,不需要自进化处理。之后执行打标签,分为自动标签 + 人工复核标签。自动标签依据工具返回结果、会话终止状态、用户反馈标记成功失败,自动推断根因是规划错误、参数解析错误、工具选错;自动标签存在误差,高价值 bad case 人工复核修正标签。之后做去重与多样性采样,对语义高度重复的会话去重,避免数据集大量重复样本;维持正负样本合理比例,不能全部是失败样本,保留部分高质量成功轨迹。最后做数据分层,分为普通原始数据、候选进化数据集、黄金回归集;只有候选进化数据集允许送入自进化流程,黄金回归集永久保存用于回归评测。

判断一条 Agent 交互轨迹是否高质量,有五条判定维度。第一完整性,轨迹链路完整,从用户输入到会话结束全链路信息齐全,包含思考过程、Skill 调用入参出参、中间状态;中断残缺轨迹为低质量。第二根因标签准确,可以明确区分失败来自 Agent 策略层面,还是外部依赖底层 bug;标签混淆的数据会误导自进化。第三低噪声,用户意图清晰,不存在乱码、无意义输入、注入攻击。第四业务代表性,样本代表一类真实业务场景,不是偶然偶发异常。第五沙箱可复现,把该 query 放到沙箱环境运行,可以稳定复现原来成功或者失败现象;线上偶现不可复现的轨迹质量较低,不适合用于自进化。

记忆法选用来源‑加工‑质检记忆法:数据来源线上轨迹、人工构造、沙箱仿真;加工链路:埋点采集、脏数据过滤、打标签、去重采样;质检维度:完整性、标签准确、低噪声、业务代表性、可复现。

采用 SFT 或者强化学习怎么来解决 Agent 调用工具不正确的问题?

Agent 工具调用不正确常见现象:选错 Skill、入参缺失、参数格式错误、工具调用顺序错误、不该调用工具的时候强行调用。SFT 监督微调与 RL 强化学习是两种不同优化路径,二者定位不一样,不能互相完全替代,并且都有明确适用边界,不能遇到工具调用错误就直接微调。面试加分点,先说明哪些场景不适合 SFT/RL,再分别讲 SFT 怎么做、RL 怎么做,同时讲风险。

首先做前置判断,如果工具调用错误根因是 Skill 描述写的差、提示词约束不足、Few‑Shot 样例缺失、RAG 召回错误、Skill 底层接口 bug,优先修复提示词与业务逻辑,不要直接上 SFT 或者强化学习。只有经过充分 Prompt 优化之后,同类工具调用错误依旧高频复现,问题来源于模型本身推理输出行为,才考虑 SFT 或者 RL。

SFT 监督微调的思路,收集大量高质量正确 Agent 交互轨迹,整理成 SFT 训练样本。每条样本输入侧是完整上下文:system 提示词、用户 query、历史对话、Skill 工具描述;输出侧是模型正确的思考、正确工具调用 JSON。训练目标是让模型学习模仿正确的工具调用行为,给定输入上下文,拟合出标准正确输出。

SFT 数据集质量至关重要,只能使用工具调用完全正确的轨迹;如果混入错误调用样本,模型会学习错误行为,进一步恶化工具调用效果。数据集需要兼顾多样场景,包含单工具调用、多工具串行调用、不需要调用工具直接回答的 case,正负样本配比均衡。SFT 优点实现相对简单,训练稳定,不容易出现模式崩溃;缺点,只能模仿已有的样本行为,没有奖励引导,无法主动区分好坏,遇到训练集之外全新场景,依旧可能工具调用出错。SFT 解决的是 "学会模仿正确格式与行为"。

强化学习 RL,典型是 RLHF/RLAIF 范式,分为奖励模型训练、PPO 强化学习两个阶段。 第一步训练奖励模型 RM。收集 Agent 生成的多条不同工具调用结果,可以是正确调用、选错工具、参数错误等样本,人工或者 AI 裁判给不同输出打分排序,训练奖励模型,输入完整上下文 + Agent 输出,输出一个标量奖励分数。工具调用正确、参数完整合理奖励高;选错工具、参数缺失奖励低。 第二步 PPO 强化学习,以 SFT 模型作为初始模型,输入上下文让模型生成工具调用,奖励模型给出 reward,配合 KL 散度约束,更新模型参数,最大化奖励,同时约束模型不要和原始 SFT 模型差距过大,防止模型崩坏。强化学习优点,不依赖大量标准答案样本,可以通过奖励信号引导模型避开错误工具调用行为;缺点训练复杂、调参难度大、成本高,容易出现奖励黑客现象,模型找到欺骗奖励模型的输出,表面分数高实际工具调用依旧错误。

工程落地一般采用组合方案:优先 Prompt 优化;之后做 SFT 教会模型工具调用格式与基础行为;再选择性使用 RL 进一步优化行为偏好。二者都必须配合评测 Harness 沙箱回归,防止微调之后其他业务场景能力退化。

伪代码 SFT 样本格式示例

复制代码
messages = [
{"role":"system","content":"你拥有工具skill_a、skill_b,按照格式调用工具"},
{"role":"user","content":"查询张三报销"},
{"role":"assistant","content":"需要调用报销查询工具{"name":"skill_a","parameters":{"name":"张三"}}"}
]

记忆法选用SFT 模仿‑RL 奖励引导记忆法:SFT 学习模仿正确工具调用行为,依赖高质量正确样本;RL 依靠奖励信号区分好坏,训练复杂存在奖励黑客;优先优化 Prompt,SFT 做基础,RL 做进阶优化。

介绍你项目的数据获取渠道、训练硬件资源配置、奖励函数的设计思路与调优逻辑。

AI Agent Function Call 项目的数据获取渠道分为五类,分别是公开数据集、线上业务日志、模板与 LLM 自我仿真、人工标注数据集、半自动化修正数据集。公开数据集,使用开源 Function Call 数据集,获取基础工具调用样本,这类数据格式规范,但用户意图偏向简单,缺少业务专属工具,只适合做基础打底,不能直接上线使用。线上业务日志,生产环境收集真实用户交互轨迹,包含用户原始 query、Agent 思考、工具调用、工具返回、最终回答。原始日志质量参差不齐,存在大量错误调用、循环调用、无效交互,需要清洗过滤,剔除违规、会话断裂样本;可以从中提取真实用户分布,获取真实用户模糊提问、复杂多轮诉求,这是保证模型线上泛化最重要的数据来源。模板与 LLM 自我仿真,基于工具 Schema 编写意图模板,批量生成用户 query,同时开启双角色自我对弈,一个角色模拟用户,一个角色模拟 Agent,对接工具仿真环境,生成完整多轮交互轨迹,可以快速扩充稀有意图样本,仿真工具报错、参数异常等线上容易出现但是真实日志数量少的 case。人工标注数据集,针对线上高频错误 case,组织标注人员完成完整多轮交互轨迹标注,产出高质量小样本集,用来补充难例,提升模型短板。半自动化修正数据集,拿线上失败轨迹,保留用户 query 与工具返回观测,使用大模型或者人工修正 Agent 错误推理与错误工具调用,产出修正后的正样本,同时保留部分失败轨迹作为负样本加入训练集。数据集构建过程需要控制各类意图占比,不能全部是理想正确样本,负样本、异常样本需要占有一定比例。

训练硬件资源配置,以 7B 参数 Agent 模型作为基准。SFT 监督微调阶段,使用 4 卡 A100‑80G,支持 LoRA 微调的情况下单卡也可以跑通,全参数微调需要 4 卡及以上 A100‑80G;使用 ZeRO 分布式优化降低显存占用。Agentic CPT 继续预训练,7B 模型全参数训练建议 4‑8 卡 A100‑80G,序列长度设置 4096 或者 8192,CPT 阶段序列长度更大,显存压力高于普通 SFT。GRPO 强化学习阶段显存开销最大,需要同时加载旧参考模型、策略模型,并且每组 prompt 采样多条生成样本,7B 模型 GRPO 建议 8 卡 A100‑80G,开启 LoRA 可以降低到 4 卡;如果是 13B 模型,GRPO 建议 8 卡 A100‑80G。存储侧,训练数据集、checkpoint 存放于高速 NAS 存储,checkpoint 定时保存,每若干 step 保存一次,防止训练异常中断。推理侧线上服务使用 A10 或者 A100 显卡,部署 vLLM 做连续批处理推理。

奖励函数设计思路,Function Call Agent 项目采用多维度加权混合奖励,分为结果奖励与过程奖励两大块。结果奖励包含 JSON 格式合法性奖励、工具名称匹配奖励、参数正确性奖励、工具执行后回答匹配奖励。过程奖励包含推理思考合理性奖励、参数语义校验奖励、工具调用次数约束奖励,避免无限循环调用。各个维度奖励数值统一归一化到 -1, 1 区间,不同维度设置独立权重,加权求和得到单样本总奖励。不单一依赖工具执行结果奖励,引入过程奖励缓解稀疏奖励问题,部分奖励不需要运行工具就可以计算。

奖励函数调优逻辑,首先做单维度 ablation 实验,固定其余权重,单独调整某一项奖励权重,观察验证集指标变化。如果模型大量输出 JSON 解析失败,调高格式奖励权重;如果频繁选错工具,调高工具名称奖励权重;如果参数键名正确但是参数语义错误,调高参数语义过程奖励权重。训练过程观察几个关键指标:训练奖励曲线、验证集 Function Call 准确率、非法 JSON 占比、平均工具调用轮次、奖励黑客样本占比。如果训练集奖励持续上涨,但验证集工具调用准确率下降,代表出现奖励黑客,模型过拟合奖励函数,此时降低对应奖励权重,调高 KL 惩罚系数,加大 clip 的 ε 约束。如果模型出现频繁重复调用工具,调高调用次数惩罚权重。如果模型不敢调用工具,明明需要工具却直接回答,降低调用次数惩罚,适度提升工具选择正向奖励。调优不能只看训练 loss 和训练奖励,必须看线下验证集真实业务指标,训练奖励虚高是 RL 训练非常常见陷阱。

面试关键点,硬件配置需要区分 SFT、CPT、GRPO 不同阶段显存差异,GRPO 显存开销最大。面试加分点,讲清楚奖励调优不是调完就结束,需要观察奖励黑客现象,区分训练奖励虚高和真实业务指标。记忆法使用模块拆分记忆法 ,数据渠道五类分开记忆;硬件分 SFT/CPT/GRPO 三个阶段;奖励设计分为结果 + 过程;调优记住看 4 类观测指标;辅助使用故障驱动记忆法,每一类故障对应调优手段,JSON 错调格式权重,工具选错调工具权重,奖励黑客调 KL 与 clip。

复制代码
def total_fc_reward(format_score, tool_name_score, param_score, answer_score, think_score, semantic_param_score, loop_penalty):
    w_format = 0.3
    w_tool = 0.25
    w_param = 0.2
    w_ans = 0.15
    w_think = 0.1
    w_semantic = 0.1
    w_loop = 0.08
    total = (w_format * format_score + w_tool * tool_name_score + w_param * param_score + w_ans * answer_score
             + w_think * think_score + w_semantic * semantic_param_score - w_loop * loop_penalty)
    return torch.clamp(torch.tensor(total), min=-1.0, max=1.0).item()

模型推理加速技术有哪些?LLM 推理优化做过哪些工作?用过 continuous batching、KV Cache、vLLM 吗?线上高峰吞吐量多少?

大模型推理加速可以分为模型计算优化、内存优化、调度批处理优化、硬件指令优化四大方向。模型计算优化包含权重量化、模型剪枝、稀疏化、蒸馏;内存优化包含 KV‑Cache、PagedAttention;调度批处理优化包含静态 batching、continuous batching 连续批处理;硬件指令优化包含 TensorRT‑LLM、cuBLAS、FP8 硬件加速算子。

KV‑Cache 是 LLM 最基础的推理内存优化手段。自回归生成每一步只会生成一个新 token,每一轮前向传播,历史 token 对应的 key、value 重复计算,会带来巨大计算冗余。KV‑Cache 把每一层注意力的 key、value 张量缓存保存,后续解码步骤直接复用历史 KV,只对最新输入 token 计算 KV,大幅减少计算量,降低解码阶段算力消耗。上下文越长,KV‑Cache 带来收益越大。KV‑Cache 原生存在缺陷,不同请求 KV 长度各不相同,内存碎片化严重,传统实现需要提前预分配最大序列长度内存,内存利用率低下。

Continuous Batching 连续批处理,也叫 in‑flight batching。传统静态 batching,必须等待一个 batch 内部所有请求全部生成完毕之后,再送入下一批请求。请求生成输出 token 数量长短不一,长生成请求会阻塞整个 batch,GPU 算力大量空闲。Continuous Batching 不再按完整请求做 batch,而是以 token 为粒度调度,只要 GPU 有空余算力槽位,就把新请求加入,同时继续处理还没有完成解码的旧请求,实现请求之间调度交错执行,GPU 利用率大幅提升。vLLM 框架底层就是基于 PagedAttention 实现 Continuous Batching。PagedAttention 借鉴操作系统虚拟内存分页思想,把 KV 缓存切分为固定大小 page 块,KV 不需要连续内存空间,允许 KV 分散存放在不连续物理内存页,解决 KV‑Cache 内存碎片问题,极大提升内存利用率,支撑更高并发。

工程落地中做过的 LLM 推理优化工作,第一,接入 vLLM 推理服务,替换原生 Hugging Face transformers 推理后端,开启 PagedAttention 与 continuous batching,开启权重 AWQ 量化,降低显存占用。第二,优化 prompt 模板,精简系统 prompt,控制输入上下文长度,避免过长上下文推高 KV 内存占用。第三,配置动态 max_tokens,根据用户输入动态限制最大生成长度,防止个别长输出请求耗尽 GPU 显存。第四,开启请求限流与队列机制,高峰对请求做排队,防止 OOM。第五,调整 vLLM 调度参数,调整 max_num_batched_tokens、max_paged_num_seqs,平衡延迟与吞吐量。第六,做请求冷热区分,高频 Agent 工具调用 prompt 做 prompt 缓存,重复系统 prompt 部分直接复用 KV 缓存,减少重复计算。第七,做输出截断、超时控制,卡住的推理请求及时终止,释放显存资源。

线上吞吐量数据会受模型大小、输入输出平均长度、量化精度、GPU 型号影响。以 7B 模型,A100‑80G,AWQ4bit 量化,vLLM 部署,Agent 业务场景,输入平均 512token,输出平均 256token,高峰 QPS 大约 25‑35,token 吞吐量每秒可以达到 12000‑18000 token。13B 模型相同硬件 4bit 量化高峰 QPS 大约 12‑18,token 吞吐量 6000‑10000 token/s。需要说明吞吐量是业务实测,和输入输出 token 长度强相关,如果上下文变长吞吐量会明显下降。

面试关键点,分清 KV‑Cache 解决重复计算;continuous batching 解决 GPU 算力空闲;PagedAttention 解决 KV 内存碎片。面试加分点,能够讲清楚三者关系:KV‑Cache 是基础优化;continuous batching 是调度层优化;PagedAttention 是 vLLM 实现 continuous batching 的内存底层机制。记忆法使用层级记忆法 ,四层:计算优化、内存优化 (KV‑Cache)、调度优化 (continuous batching)、内存分页 (PagedAttention/vLLM);辅助使用数值锚定记忆法,记住 7B 4bit A100 业务大致吞吐量区间,面试可以直接描述业务实测区间。

复制代码
# vLLM简易服务调用示例
from vllm import LLM, SamplingParams

sampling_params = SamplingParams(temperature=0.01, max_tokens=512)
llm = LLM(model="./agent‑7b‑sft‑rl", quantization="awq", gpu_memory_utilization=0.85)
prompts = ["<|system|>你是Agent助手<|user|>查询广州天气<|assistant|>"]
outputs = llm.generate(prompts, sampling_params=sampling_params)

模型剪枝 / 量化(GPTQ、AWQ)、服务化框架(FastAPI + vLLM)相关技术了解吗?

模型剪枝是通过移除模型部分权重参数,达到减小模型体积、降低计算量、加速推理的技术。分为非结构化剪枝与结构化剪枝。非结构化剪枝,把权重矩阵里面单个权重置零,权重稀疏,但是硬件很难利用稀疏性加速,通用 GPU 上收益有限,更多用于学术研究。结构化剪枝,按整行、整列、整个注意力头、整个 FFN 层做裁剪,直接移除完整结构,权重矩阵维度下降,可以直接获得推理加速,但是会带来模型能力损失,剪枝比例越高,性能退化越明显。大模型 Agent 业务中,剪枝落地并不普遍,剪枝之后需要做重微调恢复精度,流程复杂;行业更多优先选择量化来做压缩,剪枝一般作为辅助手段。

量化技术,将模型权重从高精度浮点数转换为低精度数值,降低权重存储体积,减少显存占用,提升推理速度。GPTQ 与 AWQ 都属于权重量化技术,只对权重做低精度,激活值保持 FP16,即 W‑A16 量化。GPTQ 属于基于二阶信息的后训练量化算法,不需要 finetune,利用少量校准数据,逐权重列求解量化误差最小化,逐层量化权重矩阵。GPTQ 对 4bit 量化效果不错,缺点是量化过程耗时较长;推理的时候需要权重反量化回 FP16 做计算。AWQ 是激活感知权重量化,核心观察,部分权重对激活输出影响远大于其余权重,AWQ 会保护这部分重要权重,不对其做过度量化,通过权重缩放调整,降低量化带来的精度损失。同等 4bit 条件下 AWQ 的模型效果普遍优于 GPTQ,尤其 Agent、Function Call 这类对细节指令敏感场景,AWQ 退化更小。AWQ 量化速度快于 GPTQ,vLLM 原生支持 AWQ 与 GPTQ 权重加载。量化存在精度边界,2bit 量化会出现显著能力退化,Agent 业务一般优先 4bit;8bit 量化损失很小,但是显存压缩收益有限。

FastAPI 加 vLLM 是工业界常用的大模型线上服务化组合。vLLM 本身实现高性能推理内核,对外提供基础 http 服务,但是工程上一般外层封装 FastAPI 业务网关。vLLM 负责底层模型推理,完成 PagedAttention、continuous batching、量化权重加载、token 生成;FastAPI 作为上层业务服务网关,承担业务逻辑处理。FastAPI 承担的职责包含请求鉴权、参数校验、请求队列管理、限流、熔断超时控制、请求日志埋点、业务 prompt 模板拼装、错误异常捕获、多模型路由、返回结果业务后处理。

如何优化大模型在长文本生成中的显存占用?

长文本场景显存开销主要来自三大部分,模型权重本身、中间激活值张量、KV‑Cache 缓存,长上下文下 KV‑Cache 通常会成为显存最大消耗项,优化也围绕这三块展开。权重层面的优化优先使用权重量化,AWQ、GPTQ 实现 4 比特权重量化,权重从 FP16 压缩到 4bit,权重显存直接压缩四分之三,推理阶段权重常驻显存,长文本场景下可以腾出大量显存空间留给 KV 缓存,8 比特量化损失更小但压缩比有限,2 比特量化会带来明显能力退化,Agent 长文档解析业务优先选择 4bit AWQ。权重分片与模型并行,多卡环境使用张量并行,把权重矩阵切分到多张 GPU,单卡只保存部分权重,降低单卡权重显存占用;流水线并行适合超长序列训练场景,把不同网络层分配到不同显卡,分摊激活显存压力。

KV‑Cache 是长文本推理显存消耗大头,原生 KV‑Cache 会为每一个请求分配连续内存,长序列下内存碎片严重。PagedAttention 借鉴操作系统虚拟内存分页,将 KV 缓存切分为固定大小内存页,不需要连续物理内存,内存页可以复用、回收,大幅降低内存碎片,显著提升长文本并发能力,vLLM、TensorRT‑LLM 都实现该机制。开启 KV 缓存复用,针对重复的系统提示词、固定前置上下文,多个请求共享同一份 KV 缓存,不需要每个请求重复计算与存储,在 Agent 业务中系统 prompt 固定,该优化收益很高。KV 缓存量化,将 KV 的 key、value 张量做 FP8 或者 INT8 量化,不改动模型权重,直接压缩缓存体积,长序列收益突出,需要注意量化会引入微小精度损失,需要做业务指标验证。实现滑动窗口 KV 缓存,对于超长输入,只保留最近 N 个 token 的 KV,丢弃距离当前较远历史 KV,适合允许部分久远历史信息衰减的业务,会牺牲部分长距离召回能力,不适合要求完整全文记忆的场景。

激活值显存优化更多面向微调训练阶段,长文本 SFT、CPT 训练时激活会随序列长度平方增长。梯度检查点技术,选择性丢弃部分中间激活,反向传播时重新前向计算,以少量计算开销换取显存下降,序列越长收益越明显,长文本微调几乎都会开启梯度检查点。LoRA 微调,只训练少量低秩矩阵,冻结基座全部权重,不需要保存基座权重梯度、优化器状态,优化器显存开销大幅下降,对比全量微调,长序列训练显存压力显著降低。选择更高效的注意力实现,例如 FlashAttention,利用 GPU 高速 SRAM 分块读写,不把完整注意力矩阵写入 HBM 显存,规避注意力矩阵 O (n²) 显存爆炸问题,长文本训练推理必备,FlashAttention‑2、FlashAttention‑3 进一步优化分块逻辑。

业务侧工程优化同样不可忽略,设置合理的最大上下文长度,做好请求输入截断,业务允许的前提下对输入文档做摘要、过滤冗余文本,减少真实送入模型的 token 数量;做好请求管控,对超长输入请求做限流,防止少数长请求耗尽全部 GPU 显存;推理侧使用 continuous batching 连续批处理,配合 PagedAttention 实现内存页回收,请求结束立刻释放 KV 内存页,避免内存泄漏。

面试关键点,区分训练阶段显存优化和推理阶段显存优化,KV‑Cache 相关优化主要作用推理,梯度检查点、FlashAttention 训练推理都生效。面试加分点,可以指出长文本场景显存瓶颈往往不是权重,而是 KV 缓存与注意力中间张量,很多人错误只关注权重量化。记忆法采用分层拆解记忆法 ,分为权重层、KV 缓存层、激活‑注意力层、业务工程层四个维度记忆;辅助使用瓶颈溯源记忆法,记住长文本显存三大来源:权重、激活、KV‑Cache,每一类对应一组优化手段。

复制代码
# FlashAttention + 梯度检查点训练伪代码片段
from transformers import AutoModelForCausalLM
model = AutoModelForCausalLM.from_pretrained(
    "./base_model",
    attn_implementation="flash_attention_2",
    torch_dtype=torch.bfloat16
)
model.gradient_checkpointing_enable()

如何解决大模型 API 服务的响应延迟问题?

大模型 API 服务响应延迟由多个环节共同组成,包含网络传输延迟、网关业务逻辑耗时、排队等待调度延迟、模型首 token 生成延迟 (TTFT)、token 生成解码延迟。优化也按照链路分层,分别解决排队、首 token、解码速度、业务逻辑、网络层面问题。

排队调度延迟,当请求量超过服务处理能力,新请求进入队列等待 GPU 资源,是线上 API 延迟抖动最常见来源。采用 continuous batching 连续批处理,替换传统静态批处理,以 token 粒度调度请求,GPU 空闲槽位立刻接纳新请求,大幅降低排队等待时间,vLLM、TensorRT‑LLM 原生支持该机制。配置合理的队列长度,设置最大排队请求上限,超出阈值直接返回限流,防止队列无限堆积造成整体延迟雪崩;做好服务多实例水平扩容,根据监控 QPS、GPU 利用率做弹性扩缩容,多实例分担流量压力。做请求优先级调度,Agent 业务中区分高优先级线上流量与低优先级测试流量,高优请求优先调度,保障核心业务延迟。

首 token 生成延迟 TTFT,代表从请求送入模型到返回第一个输出 token 耗时,Prefill 预填充阶段,一次性处理全部输入 prompt 序列,计算复杂度 O (n²),输入 prompt 越长 TTFT 越高。优化 Prefill 阶段计算速度,使用 FlashAttention 加速预填充计算;开启权重量化 AWQ/GPTQ,降低访存开销;张量并行多卡拆分模型权重,加速 prefill 矩阵运算。实现固定系统 prompt 的 KV 缓存复用,Agent 场景大量请求共用相同 system 提示词,提前计算好这部分 KV 缓存,请求到来直接复用,不再重复 prefill 系统 prompt,显著降低 TTFT。业务侧精简 prompt,剔除冗余文本,压缩输入 token 长度,在业务允许下对长输入做摘要压缩。

解码阶段 token 生成延迟,每个 token 生成速度,受权重访存、算力、KV 缓存影响。开启 PagedAttention 优化 KV 缓存内存管理,减少内存碎片;使用权重量化降低权重访存压力;调整 vLLM 参数 gpu_memory_utilization 合理拉高,充分利用显存;合理设置 max_num_batched_tokens,平衡并发数与单 token 生成速度。避免单条请求设置过大 max_tokens,防止长输出请求长期占用 GPU 资源拖慢其余请求。

网关与业务链路优化,外层 FastAPI 网关避免同步阻塞 IO,全部使用异步 async 接口;耗时代码放到异步任务,不要阻塞推理请求链路;精简鉴权、日志、参数校验逻辑,去掉不必要的计算;prompt 模板拼装提前预处理,不要请求到来时做复杂文本处理。网关和推理服务部署在同一个内网机房,降低内网网络 RTT,避免公网转发带来额外时延。

流量与业务策略优化,做请求缓存,对于完全相同输入 query,缓存模型返回结果,命中缓存直接返回,绕过模型推理;针对非强实时业务,提供流式返回接口,客户端拿到首 token 即可展示,不用等待完整全部结果生成,改善用户体感;做好监控埋点,分别统计 TTFT、token 生成速度、排队耗时、网关耗时,定位延迟到底出在哪一个环节,不能笼统看总耗时。

面试关键点,区分 TTFT 首 token 延迟和解码 token 延迟,二者优化手段不完全一样。面试加分点,能够说明很多线上延迟不是模型本身慢,而是排队堆积导致,优先看 GPU 利用率和队列长度指标。记忆法采用链路顺序记忆法 ,按照请求链路顺序:网络→网关排队→Prefill (TTFT)→解码生成,每一段对应一组优化手段;辅助使用指标锚定记忆法,记住两个核心指标 TTFT、token/s,围绕指标反推优化。

复制代码
# FastAPI异步服务简易框架
from fastapi import FastAPI
from pydantic import BaseModel
app = FastAPI()

class Req(BaseModel):
    prompt: str

@app.post("/api/generate")
async def infer(req: Req):
    result = await vllm_async_client.generate(prompt=req.prompt)
    return {"reply": result}

分支覆盖率是怎么统计的?原理有没有了解过?代码插桩具体是怎么实现的?

分支覆盖率属于软件测试白盒测试指标,衡量被测代码中分支逻辑被测试用例覆盖到的比例。语句覆盖率只统计是否执行到某一行代码,分支覆盖率关注条件判断之后不同走向是否都被执行。例如 if‑else 语句,条件表达式存在真、假两个分支;switch‑case 存在多个 case 分支;三元运算符、逻辑与逻辑或短路运算也会产生隐式分支。分支覆盖率计算公式:分支覆盖率 = 已执行到的分支数量 ÷ 全部可执行分支总数量 ×100%。全部可执行分支指代码中所有条件判断可以跳转的路径,不包含不可达死代码分支。举例,一段 if‑else 代码,总共有 2 个分支,如果测试用例只走到 if 为 true,没有走到 else,已执行分支为 1,分支覆盖率等于 50%。

底层实现原理核心思路,在代码的每一处分支跳转位置植入埋点,程序运行执行到对应分支时记录该分支已经被命中;程序执行结束之后收集全部埋点命中记录,统计已命中分支数与总分支数得到覆盖率数值。行业主流实现手段就是代码插桩,插桩分为源码插桩与字节码插桩两类。

源码插桩,在源代码层面修改文本 AST 抽象语法树。解析源代码生成 AST,遍历 AST 识别所有条件判断节点,包含 if、else if、switch case、三元表达式、逻辑短路运算。识别每一个分支的入口位置,在每一个分支代码块的最开头,插入一条埋点函数调用语句。埋点函数功能十分简单,内部维护一个全局集合,每执行一次就将当前分支唯一标识 ID 写入集合。分支 ID 可以使用文件名 + 行号 + 分支索引作为唯一 key。修改完成之后把新 AST 重新生成源代码,编译运行修改后的源码,跑测试用例;程序结束导出集合内命中的分支 ID,与预先统计全部分支 ID 做比对,计算分支覆盖率。源码插桩缺点,需要修改源码文件,会改变代码行数,部分场景会干扰原有业务逻辑,一般用于单元测试环境,生产环境不会直接使用修改后源码。

字节码插桩不对源代码文本改动,直接对编译之后中间字节码做修改。Java 生态使用 ASM 框架修改 class 字节码;Python 环境可以修改 bytecode 字节码;C/C++ 使用 LLVM IR 插桩。以 LLVM IR 插桩举例,源代码编译生成 LLVM 中间 IR,IR 中 if‑else 会转化为 br 跳转指令,br 指令对应 true、false 两个基本块。遍历 IR 全部基本块,在每一个分支基本块起始位置插入埋点调用指令,记录分支 ID。之后再编译为机器码执行。程序运行,每当进入对应分支基本块,埋点指令执行记录命中。字节码插桩优势,不需要改动业务源码,只干预编译产物,不会污染源代码仓库;可以对第三方库字节码插桩。缺点,调试的时候堆栈信息对应原始源码行号需要映射表,堆栈阅读会有一定成本。

插桩还有动态插桩方式,程序二进制运行起来之后,在内存修改机器指令,替换部分指令为跳转埋点,不需要提前修改源码或者字节码,实现复杂度高,一般用于动态分析工具。

插桩工程需要处理几个难点,识别隐式分支,逻辑运算符 &&、|| 会发生短路,会产生隐式分支,如果漏统计会造成覆盖率计算不准;过滤不可达代码,死代码分支不计入总分支;插桩埋点代码本身不能影响业务逻辑,埋点函数必须轻量,避免性能开销;需要维护分支 ID 映射表,把运行时 ID 映射回源码文件行号,方便报告输出。

面试关键点,区分语句覆盖率与分支覆盖率,语句覆盖率 100% 分支覆盖率可能只有 50%。面试加分点,可以区分源码 AST 插桩、字节码 IR 插桩两种实现路径,说明各自优缺点。记忆法采用原理流程记忆法 :识别分支点→插桩埋点记录命中→运行程序采集数据→统计覆盖率;辅助使用案例记忆法,if‑else 只跑 if 分支,语句覆盖率 100%,分支覆盖率只有 50%。

复制代码
# 伪代码:源码插桩AST修改前
def func(a):
    if a>0:
        return 1
    else:
        return 0

# 插桩之后伪代码
branch_hit = set()
def _trace_branch(branch_id):
    branch_hit.add(branch_id)

def func(a):
    if a>0:
        _trace_branch("func:3:true")
        return 1
    else:
        _trace_branch("func:5:false")
        return 0

对于代码解析有没有前置分析?有效性判断怎么实现的?未来让你来优化这些指标你会怎么设计?

代码解析是指大模型 Agent 场景下,模型生成代码片段、工具调用代码、函数调用逻辑之后,系统对生成代码做预处理校验的整套流程。前置分析发生在代码真正执行运行之前,不执行代码,做静态扫描,拦截明显错误,避免错误代码下发执行引擎造成异常、安全风险或者资源浪费。前置分析分为语法层静态分析、结构 Schema 校验、语义轻量检查、安全风险扫描几个模块。语法层静态分析,使用对应语言的解析器,将模型输出代码解析为抽象语法树 AST。如果解析抛出语法异常,代表代码语法错误,直接判定无效,不送入执行器。例如 Python 代码使用 ast 模块尝试 parse,JSON 格式工具调用使用 json.loads 解析,捕获解析异常。结构 Schema 校验,针对 Agent Function Call 场景,解析出函数名、入参,对比预定义函数 Schema,校验函数名称是否存在,入参键名集合,参数数据类型是否匹配 schema 定义,必填参数是否缺失,识别多余非法参数。语义轻量检查,不完整执行代码,只做静态语义扫描,识别明显语义错误,例如调用不存在变量、函数传入参数数量不匹配、数组索引为负数,识别死循环风险特征,例如 while True 无退出条件。安全风险扫描,前置识别高危操作,包含文件删除、系统命令执行、网络向外发起高危请求,匹配危险函数、危险 API 黑名单,命中黑名单直接拦截,防止模型生成恶意代码带来安全漏洞。前置分析不是完整编译执行,能力有限,只能捕获浅层错误,无法识别运行期才暴露的逻辑 bug。

有效性判断分为静态有效性与运行时有效性两个层级。静态有效性依靠上面前置分析输出结果,语法解析成功、schema 校验通过、静态语义检查无致命错误、安全扫描放行,视为静态有效。静态有效不等于代码逻辑正确,只是代表代码具备可以运行的基础条件。运行时有效性,将经过前置分析放行的代码送入沙箱执行环境,沙箱做资源隔离,限制 CPU 时间、内存占用、禁止高危系统调用。执行之后采集执行结果,判断是否抛出运行时异常,是否超时,是否内存超限。无异常正常返回结果,视为运行有效;抛出异常、超时、资源超限判定运行无效。同时采集业务语义有效性,即使代码跑通返回结果,返回结果是否匹配用户原始问题诉求,这一层需要结合业务规则或者打分模型做判断。

现有体系存在的短板,前置静态分析只能抓浅层错误,大量逻辑错误静态无法识别;有效性判断依赖沙箱执行,沙箱执行存在资源开销;部分场景很难区分代码运行失败根源是模型生成错误还是输入数据问题;静态语义检查能力有限,很多隐藏 bug 只能运行之后暴露。

如果接手优化整套指标,分为静态前置分析增强、运行时沙箱优化、数据回流闭环、模型侧联动优化四个方向做设计。静态前置分析增强,扩展静态分析规则库,引入轻量静态分析工具,对 AST 做更多规则扫描,例如类型推导,常量传播,识别更多静态可以发现的逻辑风险点;针对 Agent 工具调用,增加参数语义校验,正则、范围校验,参数值是否落在业务合法区间;构建错误模式库,把线上高频失败代码抽象成 AST 模式,前置匹配拦截。运行时沙箱优化,优化沙箱隔离性能,复用沙箱实例降低启动开销;细粒度资源管控,分级超时策略;丰富执行日志,记录异常类型,区分语法错误、参数错误、逻辑错误、环境异常,方便后续定位根因。数据回流闭环,把前置分析、沙箱运行得到的有效、无效样本自动回流数据集。无效样本分为语法错误、schema 错误、运行异常、业务语义错误,自动打标签;坏样本筛选之后,一部分构造 SFT 负样本,一部分构造 GRPO 过程奖励信号,训练阶段让模型减少生成此类错误代码。模型侧联动优化,把前置分析校验逻辑一部分转化为训练阶段过程奖励函数,不需要等到推理阶段才发现错误,训练阶段就施加信号,降低模型输出错误代码概率;推理侧增加小规模校验打分模型,输入生成代码,输出有效性打分,对低分样本做二次校验或者拒绝执行。配套完善指标体系,统计语法失败率、schema 校验失败率、沙箱运行异常率、业务语义通过率,各个环节埋点统计,定位短板在哪个环节,驱动迭代优化。

面试关键点,分清静态前置分析不运行代码,运行有效性必须沙箱执行,静态有效不等于业务正确。面试加分点,能够设计完整闭环,校验结果回流到训练,形成推理校验到模型训练的负反馈闭环。记忆法采用分层记忆法 :前置分析(语法‑schema‑语义‑安全);有效性判断(静态 + 运行时 + 业务语义);优化设计(静态增强、沙箱、数据回流、模型联动);辅助使用故障溯源记忆法,记住静态分析局限性,只能抓浅层问题。

复制代码
import ast

def pre_analyze_python_code(code_text:str):
    try:
        tree = ast.parse(code_text)
    except SyntaxError:
        return {"valid":False,"reason":"语法解析失败"}
    # 遍历AST做简单静态检查
    for node in ast.walk(tree):
        if isinstance(node, ast.Import):
            for name in node.names:
                if name.name in ["os","subprocess"]:
                    return {"valid":False,"reason":"命中危险模块"}
    return {"valid":True,"reason":"静态校验通过"}

有没有思考过哪些代码会让模型生成的代码准确度和覆盖率降低?这些用 AST 和 LSP 都生成不了单测的代码如何过滤?

会拉低代码准确度、单元测试覆盖率的代码类型可以分为运行时动态特征代码、外部强依赖代码、非确定性逻辑代码、边界异常处理代码、复杂控制流代码这几大类。第一类高度动态执行代码,Python 中 eval、exec 动态执行字符串代码,getattr、setattr 动态访问属性,动态 import 导入模块,运行时才确定函数、类名称,AST 只能看到语法结构,无法在静态阶段确定真实调用对象;LSP 语言服务依赖静态符号表,符号是运行期动态生成,符号表不存在对应条目,LSP 无法解析调用关系,很难自动生成单测输入输出。第二类重度外部依赖代码,代码内部大量调用第三方 SDK、数据库、RPC 接口、文件系统、网络请求,业务逻辑深度耦合外部环境,代码逻辑结果高度依赖外部服务返回,脱离真实环境无法确定返回值,自动单测需要大量 mock 对象,AST 与 LSP 只能识别函数调用符号,无法自动构造 mock 返回数据。第三类非确定性逻辑,代码内部包含随机数、时间戳、UUID 生成,每次运行结果不固定,断言很难编写固定预期结果,自动化单测很难产出稳定断言。第四类异常分支与错误处理逻辑,大量 try‑except 多层嵌套,捕获宽泛异常基类,异常触发条件往往需要构造特殊异常场景,静态工具无法自动构造触发异常的入参,这类分支很容易变成单测覆盖盲区。第五类复杂控制流代码,多层嵌套循环、递归、大量条件分支、状态机逻辑,路径组合爆炸,静态工具很难遍历全部执行路径。第六类胶水型薄业务代码,代码只是透传参数、转发调用,本身没有业务计算逻辑,全部逻辑下沉到下游函数,单测价值本身很低,自动化工具也很难产出有效测试用例。

AST 擅长做语法树遍历、语法结构提取,但是只能拿到静态源码信息,没有运行期上下文;LSP 依赖静态符号索引,只能处理源码中显式定义的符号。遇到动态代码、外部强依赖代码,二者都无法自动生成可用单测。过滤的整套思路分为静态前置识别过滤、风险特征标记、规则 + 模型分类过滤、后置处理分流,不会直接粗暴删除代码,而是做标记分流处理。

静态前置识别,基于 AST 遍历源码,提取风险特征集合。遍历 AST 节点识别 eval、exec、动态 import、getattr、setattr 等动态执行节点;识别网络 IO、文件读写、数据库操作相关函数调用节点;识别 random、datetime 等非确定性 API 调用;识别多层嵌套 try‑except,宽泛 Exception 捕获。提取文件路径、函数名、行号,给每一个函数打上风险标签:动态执行、外部 IO 依赖、非确定性、复杂异常分支。LSP 辅助补充符号信息,识别函数内部调用的外部第三方符号,区分内部业务函数和外部依赖 SDK 调用。

规则 + 轻量分类模型做过滤分流,规则命中高风险特征之后,送入轻量分类模型,输入函数源码、AST 特征、LSP 符号信息,输出三类判定结果:可以自动生成单测、需要人工补充 mock、完全无法自动化生成单测。判定为完全无法自动化生成单测的函数,不会送入自动单测生成链路,避免产出无效、无法运行的测试代码。标记为需要人工补充 mock 的代码,依旧进入生成链路,但是在输出单测代码中插入注释标记,提示需要手动补齐 mock 对象与模拟返回值。

除了过滤,配套兜底策略,针对无法自动生成单测的代码,不在自动化流程强求覆盖率指标,将这部分函数从自动化覆盖率统计基线中排除,避免拉低整体覆盖率数值,同时输出报告,明确标记哪些函数属于排除项,供开发人员人工介入补写测试。不能直接忽略这类代码,需要形成清单,定期人工复核。

面试关键点,理解 AST 只有静态语法信息,LSP 只有静态符号信息,都没有运行时状态,这是它们无法处理动态代码的根源。面试加分点,区分 "过滤不等于删除代码",而是分流、标记、排除覆盖率统计基线,不做一刀切丢弃。记忆法采用特征‑限制‑方案记忆法 :记忆四类坏代码特征,记住 AST/LSP 的能力边界,对应识别‑标记‑分流‑排除基线整套流程;辅助使用案例记忆法,记住 eval、getattr、数据库 IO 是典型无法自动生成单测的样本。

复制代码
import ast

def detect_risk_code(source_code):
    tree = ast.parse(source_code)
    risk_flags = set()
    for node in ast.walk(tree):
        if isinstance(node, ast.Call):
            if isinstance(node.func, ast.Name):
                if node.func.id in ["eval", "exec"]:
                    risk_flags.add("dynamic_exec")
            if isinstance(node.func, ast.Attribute):
                if node.func.attr in ["getattr","setattr"]:
                    risk_flags.add("dynamic_attr")
    return risk_flags

Mock 是怎么实现的?

Mock 用于单元测试,对系统外部依赖对象做模拟替身,替换真实对象,控制它的返回值、抛出异常,隔离外部环境,让被测代码不依赖数据库、RPC 接口、第三方服务就可以完成测试。Mock 实现主要分为动态代理替换、猴子补丁、类 / 函数覆写、反射拦截这几种底层实现思路,不同编程语言实现细节有差异,Python unittest.mock 是行业最典型的实现。

猴子补丁是最常见实现手段,运行时修改模块、类的属性引用,把原有真实函数、类对象替换成自定义 Mock 替身对象。Python 中模块、类属性是运行期可变字典,导入模块之后,可以直接修改模块命名空间,把原始函数引用指向 Mock 实例。被测代码 import 对应的模块,调用的时候拿到的就不再是原始对象,而是 Mock 替身。这个过程不会修改磁盘源码文件,只修改内存运行时对象,测试结束需要恢复原有引用,防止污染后续测试用例。

Mock 替身对象本身是动态代理对象,能够记录所有调用行为。当代码调用 Mock 对象,无论传入什么参数,Mock 对象可以记录调用次数、传入的参数列表,保存调用历史;同时支持预先设定返回值,设定抛出指定异常。底层使用__call__魔法方法,让 Mock 实例可以像函数一样被调用;__getattr__魔法方法,访问 Mock 任意不存在的属性,都会自动生成子 Mock 对象,形成链式 mock,不需要提前定义全部子属性。例如 mock_obj.foo.bar.baz,每一层访问都会自动生成新的 Mock 实例,不会抛出属性不存在异常。

Mock 对象内部维护数据结构,call_args_list 保存每一次调用的参数;return_value 存储调用返回结果;side_effect 支持设置返回序列、抛出异常、自定义回调函数,相比 return_value 更加灵活。side_effect 传入异常类的时候,调用 mock 就会抛出该异常;传入迭代器,每次调用依次取出迭代器内元素作为返回值。

上下文管理器与装饰器用来管控 Mock 生命周期。使用装饰器或者 with 上下文,进入作用域执行猴子补丁替换对象,退出作用域自动回滚恢复原始对象引用,避免 mock 残留污染其他测试用例。如果手动做猴子补丁,忘记恢复引用,后面测试用例继续使用 mock 对象,会出现诡异测试结果。

其他语言实现思路,Java 中 Mockito 底层使用字节码生成,运行时动态生成目标类的子类,重写类全部方法,子类作为 mock 替身,不修改原始 class 文件;C++ 一般使用模板特化或者 gmock 做虚函数重写。

Mock 使用过程有常见陷阱,过度 mock,把被测业务代码本身 mock 掉;忘记恢复猴子补丁;side_effect 与 return_value 混用,side_effect 优先级高于 return_value;mock 对象无法匹配真实函数签名,参数不匹配不会报错,导致测试通过但真实代码运行失败。

面试关键点,猴子补丁本质运行时内存替换引用,不改动源码;Mock 对象依靠__call__、__getattr__实现动态代理。面试加分点,讲清楚 return_value 和 side_effect 区别,以及 mock 生命周期管理,忘记恢复补丁带来的问题。记忆法采用三层结构记忆法 :猴子补丁做对象替换;Mock 代理对象记录调用 + 控制输出;上下文管理器管理生命周期;辅助使用接口对比记忆法,区分 return_value 固定返回,side_effect 支持序列、异常、回调。

复制代码
from unittest.mock import patch

def query_db():
    # 真实函数,访问数据库
    pass

def business_logic():
    res = query_db()
    return res + 1

# patch做猴子补丁,替换模块内query_db
@patch("__main__.query_db")
def test_business(mock_query):
    mock_query.return_value = 10
    ret = business_logic()
    assert ret == 11
    mock_query.assert_called_once()

实际开发中使用 AI 生成代码时,如果 AI 写出了一大堆冗长、过度设计且不一定能用得上的代码,你会怎么去控制这种现象?

AI 生成代码出现冗长、过度设计,根源来自提示词约束不足,模型倾向输出完备通用方案,默认考虑大量未来扩展场景,引入很多当前需求并不需要的抽象、基类、接口、工具函数;上下文缺少现有项目代码规范、已有模块约束;模型倾向输出代码字数更多的结果,出现防御性冗余;缺少对输出代码规模、职责边界的显式限制。控制分为提示词侧约束、上下文输入增强、分层任务拆解、后处理校验过滤、模型侧迭代优化,整套手段组合使用。

提示词侧增加强约束,不能只简单写实现某个功能,需要明确写明代码约束条件。写明当前业务需求范围,明确禁止过度抽象,禁止预实现未来才有可能用到的扩展能力;明确代码风格,优先简单直接实现,不要编写通用基类、不要过度设计模式;明确输出规模,限定输出代码行数范围;明确复用现有项目已有工具函数、公共模块,禁止重复造轮子;写明输出完成之后附带简短自我检查,检查是否存在未使用导入、未调用函数、冗余抽象类。同时增加否定约束,例如 "不要编写不需要的异常捕获,不要创建不需要的接口与抽象层"。

增强输入上下文,给 AI 传入项目现有代码片段、项目工具类、内部公共 SDK、现有模块结构。AI 不了解项目已有沉淀,就会重复实现一堆已有能力。把项目工具函数签名、模块文件树、代码规范片段放入 prompt 上下文,让 AI 优先复用已有能力。如果修改已有模块,把相关原有代码完整传入,让新增代码贴合原有代码风格与复杂度,避免凭空新增一套抽象体系。

大任务做分层拆解,复杂需求不要一次性丢给 AI 生成完整大模块。把需求拆分为最小职责单元,每次只让 AI 完成单一职责小函数、小逻辑,一个子任务只做一件事。大模块拆成分步实现,先生成核心业务逻辑,再补充异常处理,再补充工具辅助函数,分步迭代,每一步限定职责范围,避免模型一次性输出大包大揽的庞大代码。如果一次性要求实现完整模块,模型就会主动脑补很多潜在场景,堆砌大量冗余抽象。

推理侧后处理校验,拿到 AI 输出代码之后做静态 AST 分析,自动识别冗余代码特征。AST 扫描识别未使用 import 导入、定义但是从未调用的函数、从未实例化的类;识别多层不必要抽象继承,定义抽象基类但是项目没有第二个子类实现该基类;识别大量分支条件,但是分支条件永远不会触发。识别到冗余特征之后,把检测结果回传给 AI,要求 AI 做精简重构,删除未使用元素,拆掉不必要抽象,保留核心业务逻辑。也可以编写简单检查脚本,统计函数、类复杂度,圈复杂度过高的片段,标记提示精简。

人工与评审流程约束,AI 输出的代码不能直接合入仓库,必须经过代码评审,评审重点检查是否存在过度设计、冗余抽象、重复造轮子。评审过程中把冗余问题反馈,再次交给 AI 迭代修改。长期维度,做模型侧优化,如果是内部微调 Coding Agent,构建负样本数据集,收集线上 AI 产出过度设计、冗长代码案例,对应的精简正确版本,构造 SFT 样本,微调模型,让模型学习项目期望的代码粒度;奖励函数层面,给简洁可运行代码更高奖励,冗长过度设计代码施加负向奖励,从模型生成源头降低该现象出现概率。

面试关键点,问题根源是模型脑补未来扩展,缺少项目上下文,任务粒度太大。面试加分点,不只讲改 prompt,覆盖 prompt、上下文、任务拆解、AST 后处理、微调闭环完整链路。记忆法采用链路记忆法 :提示词约束→补齐项目上下文→拆分小任务→AST 后处理精简→评审 + 模型微调闭环;辅助使用根源锚定记忆法,记住三个根因:脑补扩展、缺少项目上下文、任务粒度太大。

复制代码
# AST检测未使用导入伪代码示例
import ast

def find_unused_import(source):
    tree = ast.parse(source)
    imported_names = set()
    used_names = set()
    for node in ast.walk(tree):
        if isinstance(node, ast.Import):
            for name in node.names:
                imported_names.add(name.name.split('.')[0])
        if isinstance(node, ast.Name):
            used_names.add(node.id)
    unused = imported_names - used_names
    return unused

有没有尝试过让 AI 根据完整的设计文档,去开发涉及多个模块且有复杂依赖关系的长流程编程工作?

实际项目中会使用 Coding Agent 基于设计文档做多模块复杂依赖开发,但直接把完整设计文档一次性丢给大模型,几乎很难一次性产出可以直接编译运行的完整多模块工程,会出现模块接口不一致、依赖顺序错乱、数据结构定义不统一、模块之间字段对不齐、遗漏子模块等问题。设计文档驱动多模块开发核心难点,长文档上下文信息容易丢失,模型很难同时记忆全部模块接口定义;多个模块之间接口契约、数据结构体需要跨文件保持一致,模型容易出现前后不一致;模块存在依赖顺序,底层基础模块需要优先实现,上层业务模块依赖底层结构体、工具类,模型容易颠倒实现顺序;设计文档本身存在模糊点、歧义点,AI 会自行脑补实现逻辑,和真实设计意图偏离。

完整落地流程不会一次性生成全部代码,采用分阶段、契约先行的开发模式。第一阶段解析设计文档,输出项目结构契约,不直接写业务代码。让 AI 解析完整设计文档,输出项目目录结构,列出全部模块文件,明确定义每一个模块对外暴露的类、函数、数据结构体、入参出参,输出模块之间依赖关系图,明确哪些是底层基础模块,哪些是上层业务模块,模块之间调用契约。这一步产出契约文档,人工复核契约,修正接口定义、结构体字段、依赖关系,确认契约和设计文档对齐,这一步非常关键,后续所有代码生成都以这份契约作为基准,避免各模块实现各自为政。

第二阶段按照依赖顺序分批次生成代码,优先生成无依赖底层公共模块、数据模型、结构体定义、工具类。底层模块全部生成完成,人工简单校验之后,再生成依赖底层的中层模块,中层完成之后再生成上层业务流程模块。每一轮只生成有限数量模块,不要一次性全部生成,控制单次上下文长度,减少信息丢失。每生成一个模块,强制 AI 参考已经确认的契约文档,函数入参出参、结构体字段严格对齐契约。

第三阶段模块间一致性校验,全部模块生成完毕之后,做跨模块静态校验。使用 AST 与 LSP 读取全部生成源码,校验跨模块调用,A 模块调用 B 模块函数,核对函数签名、参数名称、数据结构是否和契约保持一致;检测是否存在引用未定义结构体、不存在的接口;检测模块循环依赖问题。发现不一致点,把问题信息回传给 AI,基于契约修正对应模块代码。

第四阶段增量迭代与补全,代码生成完成之后执行编译、单元测试,捕获编译错误、运行异常,把报错信息连同源码片段交给 AI,迭代修复问题。设计文档中模糊不清的地方,AI 生成代码会出现偏差,需要人工介入澄清需求,更新契约文档,再修正代码。

现实落地约束,AI 适合做契约明确的编码实现工作,不能替代人做架构决策。如果设计文档本身模糊,缺少接口定义、数据结构,AI 产出代码质量会大幅下降。对于非常复杂的长流程项目,AI 产出更多作为初稿,开发人员要做大量校验、调整、重构工作。

面试关键点,不要回答可以一次性完美生成完整项目,要客观讲局限性,强调契约先行、按依赖顺序分阶段生成。面试加分点,可以讲清楚为什么不能一次性全部生成:上下文遗忘、跨模块契约不一致、依赖顺序错乱。记忆法采用阶段流程记忆法 :解析设计文档生成契约→人工审核契约→按依赖顺序分批生成模块→跨模块一致性校验→编译报错迭代修复;辅助使用痛点记忆法,记住三大痛点:上下文丢失、跨模块契约不一致、依赖顺序颠倒。

什么是 Context Engineering?如何为开发、测试、运维等任务组织 Skills?

Context Engineering 中文一般译作上下文工程,它不是单一算法,而是一套工程实践范式,核心目标是精心构造、管理、送入大模型的全部上下文信息,让模型在不做微调的前提下,依靠输入上下文就可以稳定完成复杂 Agent 任务。传统提示词工程更多聚焦写提示文本,上下文工程范围更大,包含提示词、工具定义、示例样例、知识库片段、历史对话、技能描述、约束规则、输出格式、元信息的编排、筛选、压缩、排序、注入。它解决的现实问题是大模型上下文窗口有限,把全部信息一股脑塞进去会出现信息淹没、关键信息被忽略、token 超限、模型注意力偏移;信息太少又会缺少完成任务的必要条件。上下文工程的核心工作包含信息检索与过滤、上下文排序加权、上下文压缩裁剪、上下文结构化编排、上下文生命周期管理,区分全局常驻上下文、会话上下文、临时动态上下文。全局常驻上下文指 Agent 启动就加载,所有会话都生效的内容,例如角色定位、通用约束、全部 Skills 元描述;会话上下文属于单次对话生命周期,包含用户输入、历史交互、工具返回结果;临时动态上下文是任务运行中动态检索出来的文档、样例、报错片段,任务结束就丢弃。

Skills 可以理解为 Agent 可调用的能力单元,每个 Skill 封装一组能力描述、工具集合、执行步骤、Few‑shot 样例、校验规则,面向开发、测试、运维不同任务域,组织 Skills 需要遵循域内职责划分、依赖解耦、元信息标准化、分层编排的思路,不能把所有能力平铺堆砌在同一个上下文。面向开发任务的 Skills,拆分为代码读写 Skill、静态代码分析 Skill、单测生成 Skill、编译构建 Skill、代码评审 Skill。每个 Skill 内部结构化组织内容,Skill 元信息包含能力名称、适用任务场景、前置依赖工具、输入输出约束、2‑3 个高质量 Few‑shot 完整样例、失败处理提示。代码读写 Skill 封装文件读取、写入、修改工具;静态代码分析 Skill 集成 AST 解析、LSP 调用能力,输入代码片段,输出风险点;编译构建 Skill 调用本地编译命令,捕获编译报错,把报错信息结构化返回给 Agent。开发域 Skills 编排时,全局上下文只放所有 Skill 简短元描述列表,当 Agent 判定需要启用某一个开发 Skill,再把该 Skill 完整详细描述、样例动态注入本轮上下文,避免一次性把全部 Skill 完整内容占满上下文窗口。

面向测试任务的 Skills,分为单测生成 Skill、mock 构造 Skill、用例执行 Skill、覆盖率统计 Skill、缺陷分析 Skill。单测生成 Skill 包含单测编写规范、mock 使用样例、输出格式约束;用例执行 Skill 封装执行单元测试命令,解析测试输出报告,提取失败用例、报错堆栈。测试任务执行时,不会把开发相关 Skill 完整内容加载进来,仅保留元索引,按需加载测试域 Skill 详情,减少上下文噪声。

面向运维任务的 Skills,分为日志采集 Skill、指标查询 Skill、服务状态检查 Skill、配置修改 Skill、故障根因分析 Skill。运维 Skill 需要增加安全约束描述,明确高危操作拦截条件,例如删除、重启操作必须二次确认。运维场景动态上下文会注入实时日志片段、监控指标数据,属于临时上下文,会话结束释放。

上下文工程在 Skills 组织上一个重要原则就是分层加载,全局只保存 Skill 目录元数据,包含 Skill 名字、简短用途,当 Agent 规划任务判断需要使用某个 Skill,才将该 Skill 的完整 prompt、样例、规则注入当前轮上下文。如果全部 Skill 完整内容常驻上下文,会造成上下文拥挤,模型容易混淆不同 Skill 规则,同时消耗大量 token。同时还要做上下文净化,每一轮结束清理过期、无效的工具返回、冗余报错,防止上下文无限膨胀。

面试关键点,区分提示词工程和上下文工程,提示词工程是子集,上下文工程包含信息筛选、编排、生命周期管理。面试加分点,讲清楚 Skills 不要全部展开常驻上下文,采用元索引 + 按需注入的模式,解决上下文窗口限制。记忆法采用三层上下文记忆法 :全局常驻、会话上下文、临时动态上下文;辅助使用Skill 组织记忆法:按领域拆分 Skill,全局存元索引,任务触发才注入完整 Skill 内容。

复制代码
# Skill元数据结构伪代码
skill_catalog = [
    {
        "skill_name":"unit_test_gen",
        "brief":"根据源码生成单元测试",
        "domain":"test",
        "need_inject":False
    }
]

def inject_skill_context(skill_name):
    for skill in skill_catalog:
        if skill["skill_name"] == skill_name:
            skill["need_inject"] = True
            return skill["full_prompt"]
    return ""

如何用 Agent 做 Auto Research?

Auto Research 也就是自动调研 Agent,目标是给定一个调研主题,Agent 自主完成信息收集、资料阅读、信息交叉验证、整理归纳,最终输出完整调研报告。它和普通问答最大区别,不是单轮检索回答,而是多轮闭环任务,包含问题拆解、信息搜集、阅读解析、可信度校验、迭代补全信息、内容整合输出,整个过程自主循环,直到信息足够支撑产出报告或者判定资料不足。Auto Research Agent 典型架构包含规划模块、检索工具集、阅读解析模块、事实校验模块、收敛判断模块、报告生成模块。

规划模块接收用户原始调研主题,首先做子问题拆解。原始调研主题往往宽泛模糊,Agent 需要把大问题拆解成若干可调研子问题,生成调研大纲。例如调研某个技术方案,拆分为技术原理、优势、局限性、落地案例、对比竞品方案。规划阶段同时明确信息来源约束,区分官方文档、技术博客、行业论文、公开项目仓库,标记哪些信息需要优先采信权威来源。拆解出来的子问题会生成待调研队列,队列记录子问题、调研状态、已经收集到的资料。

检索工具集承担信息获取能力,内置网页搜索、文档读取、github 仓库读取、论文检索工具。Agent 从待调研队列取出子问题,生成检索 query,调用搜索工具获取一批候选链接。这里需要做 query 多样性处理,同一个子问题生成多条不同角度检索词,避免搜索结果信息单一。拿到网页链接之后,不能直接把完整网页全部塞进上下文,调用阅读解析 Skill,网页内容做清洗,去除广告、导航栏噪声,做文本分块,长文档做摘要提取,只保留和当前子问题相关片段,避免 token 爆炸。

事实校验模块是 Auto Research 非常关键的部分,防止 AI 直接编造内容。针对收集到多条资料片段,做交叉比对,同一事实多个来源是否说法一致;标记信息来源,每条结论带上来源引用;如果不同资料存在冲突,Agent 需要再次发起检索,进一步核实冲突点,不能直接采信某一个单方面说法。对于缺少来源支撑的观点,标记该信息可信度低,报告中注明信息未充分验证。

收敛判断模块用来决定调研循环什么时候停止,避免无限循环检索。收敛判断几个条件:待调研子问题队列全部完成;收集到的信息已经覆盖全部大纲要点;多次检索没有获取到新增有效信息;达到最大工具调用轮次上限。满足任意收敛条件就退出调研循环。如果发现关键子问题始终找不到有效资料,需要在报告中标明该部分资料缺失。

报告生成阶段,整合全部经过校验的信息,按照调研大纲组织输出完整报告,每条观点附带对应的来源标记。同时输出调研过程总结,说明哪些点信息充分,哪些点信息不足存在局限。

工程落地会遇到很多现实难点,搜索返回大量低质量网页噪声,需要过滤广告、低质量自媒体;长网页内容大,必须做分块摘要;很容易出现编造事实,所以交叉校验、来源追踪必不可少;容易无限循环搜索,必须严格的收敛终止条件。实践中不会让 Agent 无限制自由调研,设置最大搜索轮次,设置每一个子问题最多检索次数。

面试关键点,Auto Research 不是简单搜索 + 总结,重点在于子问题拆解、交叉事实校验、收敛终止逻辑。面试加分点,可以说明最大风险是 AI 幻觉编造调研结论,解决方案是来源追踪、多源交叉对比。记忆法采用流水线记忆法 :原始主题→拆解子问题生成调研大纲→检索 + 文档解析→多源事实校验→收敛判断→输出带来源报告;辅助使用风险锚定记忆法,记住两大风险:幻觉编造、无限循环检索,对应校验模块、收敛终止条件。

复制代码
# 调研子任务队列伪代码
research_queue = [
    {"sub_question":"技术原理","status":"pending","evidence":[]},
    {"sub_question":"局限性","status":"pending","evidence":[]}
]

def check_converge(queue, max_round, current_round):
    all_done = all(item["status"]!="pending" for item in queue)
    no_new_info = all(len(item["evidence"])>0 for item in queue)
    if all_done or current_round >= max_round or no_new_info:
        return True
    return False

如何记录每轮实验的耗时、Token 消耗、结果和失败路线?

AI Agent 实验迭代过程,如果没有标准化实验记录,不同版本 Skill、提示词、模型参数的实验结果容易混淆,复现困难,无法定位改动带来的好坏变化。完整记录体系分为实验元信息、运行时指标采集、轨迹记录、结果标注、失败路径留存、持久化存储、查询看板几个部分,所有数据结构化存储,不依靠人工复制粘贴记录。

每一轮实验启动首先生成唯一实验 ID,作为整条实验记录主键。实验元信息预先记录,包含实验版本标签,改动点说明,例如 "修改 Reward 权重、更新 Skill 描述、调整 prompt";基座模型版本;温度、top_p 等生成参数;数据集版本;时间戳;执行人信息;环境硬件信息。元信息全部结构化存入记录,保证后续可以复现实验条件。

运行过程自动采集指标,耗时指标分为细分阶段耗时,不要只记录总耗时。记录 Agent 规划阶段耗时、工具调用网络耗时、模型每轮生成耗时、完整会话总耗时。每一次调用大模型 API,采集输入 token 数量、输出 token 数量,累计单条样本 token 消耗,以及整个实验批次总输入输出 token。token 消耗要区分模型推理 token、工具返回带入上下文的 token,方便评估成本开销。这部分指标通过埋点在 Agent 框架钩子函数采集,每一轮 LLM 调用完成回调钩子,自动写入指标,不依靠人工统计。

完整轨迹记录,保存 Agent 每一轮完整交互轨迹,用户输入、模型思考内容、工具调用参数、工具返回内容、Agent 最终输出。失败路线是轨迹中最重要的部分,需要完整留存失败链路,不只是记录最后错误结果。例如 Agent 在哪一轮做出错误工具选择,中间哪一步推理产生错误,后续如何一步步走向失败,完整保存每一步中间状态。单纯只保存最终失败结果,很难复现定位根因。轨迹存储可以存储 JSON 格式,长文本可以对象存储保存完整轨迹文件,数据库保存索引。

结果部分分为客观指标与人工评估标签。客观指标:任务是否完成,成功 / 失败状态;工具调用次数;是否出现幻觉;JSON 解析错误次数;循环调用次数。同时保存参考标准答案,用于自动评价脚本计算指标。人工评估部分,标注失败根因分类,分为提示词缺陷、Skill 能力不足、检索信息错误、模型本身能力不足、环境工具报错,给失败路线打上分类标签,方便后续批量统计哪一类问题占比最高。

持久化存储分层设计,结构化元数据、指标、标签存入关系型数据库,方便筛选查询;完整大体积交互轨迹 JSON 存对象存储,数据库只保存文件路径。建立简单查询看板,可以按实验 ID、版本标签、改动点筛选实验,查看批次成功率、平均耗时、平均 token 消耗,查看失败案例轨迹。

工程落地注意点,需要做钩子机制,侵入性尽量低,Agent 框架每一轮推理、每一次工具调用触发回调,自动采集数据,避免业务代码大量埋点。禁止只记录成功样本,失败样本的完整轨迹价值往往高于成功样本。同时做好数据去重,同一个样本多次实验保留多条记录,方便对比不同版本表现。

面试关键点,失败路线不是只存报错,要保存完整中间推理步骤,复现失败发生过程。面试加分点,区分总耗时和分阶段细粒度耗时,区分单样本和批次 token 统计。记忆法采用实验生命周期记忆法 :实验初始化记录元信息→运行钩子采集耗时、token→全轨迹留存保存失败路径→客观 + 人工标注结果→数据库 + 对象存储持久化;辅助使用字段分类记忆法:元信息、性能指标、轨迹、结果标签,四大类记录内容。

复制代码
# 实验记录数据结构伪代码
experiment_record = {
    "exp_id":"exp_00123",
    "meta":{"model":"agent‑7b‑v2","change_note":"update skill prompt"},
    "metrics":{
        "total_cost_ms":12400,
        "plan_cost_ms":2100,
        "total_input_token":1420,
        "total_output_token":360
    },
    "trace_path":"oss://exp‑trace/exp_00123.json",
    "final_result":{"success":False,"fail_type":"skill_defect"},
}

自动更新 AGENTS.md 或 Skills 后,怎样验证效果确实提升?如何通过评价脚本或模型评审对比两个 Skill 的输出?

AGENTS.md 一般用来存放 Agent 角色定义、约束、Skills 描述集合,当修改 AGENTS.md 文本、新增或者调整 Skill 描述、Few‑shot 样例、约束规则之后,不能仅凭少量肉眼样例就判定效果提升,必须一套标准化对比验证流程,防止改动之后局部样例变好,大量其他样本退化。整套验证分为基准测试集准备、双版本对照实验、客观脚本评价、模型评审打分、人工抽样复核、指标对比、回归校验。

首先准备固定基准测试集,测试集需要覆盖正常用例、边界用例、历史失败 case,测试集固定不变,后续所有版本迭代都使用同一套测试集,保证对比公平。测试集每条样本包含用户输入 query,任务类型,期望输出或者期望工具调用行为。测试集不能只挑选简单样本,重点纳入历史线上真实失败样本,这部分样本最能体现 Skill 改动带来的变化。

执行对照实验,维护两套 Agent 运行环境,一套使用修改前旧版本 AGENTS.md/Skills 作为基线版本,另一套使用更新之后新版本。两套环境除 AGENTS.md、Skill 文件之外,模型、超参、工具、检索配置全部保持完全一致。同一套基准测试集,分别跑基线版本和新版本,保存两套完整实验轨迹记录。不能分批跑,避免环境波动带来干扰,尽量批次并行执行。

评价脚本做客观指标计算,评价脚本基于代码规则、Schema 校验、期望行为做自动化判断。针对工具调用类 Skill,脚本校验输出 JSON 格式合法性,工具名称、参数是否符合 Schema;校验是否调用期望工具,是否调用多余无关工具;校验工具调用轮次是否在合理区间;检测是否循环调用、重复调用;对比期望输出,检查关键信息是否出现在 Agent 最终回答。脚本输出结构化指标,包含批次成功率、格式错误率、多余工具调用占比、平均调用轮次。客观脚本优势速度快,可以批量跑几百条样本,缺点只能检查格式、结构、规则,无法判断语义、推理质量。

模型评审用来处理脚本无法判断语义质量的场景,调用一个能力更强的评审大模型,把基线版本输出、新版本输出、用户原始 query、任务要求一起送入评审模型。设置明确打分 prompt,要求评审模型从几个维度打分:任务完成度、推理正确性、输出简洁度、是否产生幻觉、工具调用合理性。可以采用盲测模式,打乱新旧输出顺序,避免评审模型产生顺序偏见。输出结构化打分,每条样本给出新版本优于基线、持平、劣于基线三种判定,同时输出评审理由。模型评审可以处理语义层面对比,但存在评审幻觉,不能完全信任。

人工抽样复核,在脚本和模型评审结果之上抽样。重点抽样模型评审判定新版本优于基线和劣于基线的样本,全部人工复核;随机抽取一定比例持平样本做复核。统计真实的变好、变差、无变化样本占比。如果新版本整体变好占比显著高于变差占比,同时没有出现大规模样本退化,才认为本次 AGENTS.md 或者 Skill 更新带来效果提升。如果改动后部分 case 变好,但是大量历史 case 退化,属于负向改动,需要回滚。

回归校验,确认版本提升之后,新版本纳入回归测试,后续每一次修改 AGENTS.md、Skills,都必须跑这套基准集,防止后续改动把已修复问题重新引入。

面试关键点,不能拿个别样例证明效果提升,必须固定基准集,控制变量做对照实验。面试加分点,说明客观脚本擅长格式结构校验,模型评审擅长语义,二者结合,同时需要人工抽样校验,规避评审模型幻觉。记忆法采用对照实验流程记忆法 :固定基准测试集→基线 & 新版本对照跑实验→评价脚本客观指标→模型评审语义打分→人工抽样复核→回归测试;辅助使用能力边界记忆法,脚本看结构格式,模型评审看语义,都需要人工兜底。

复制代码
# 模型评审输入构造伪代码
review_prompt = """
用户输入:{query}
任务要求:{task_requirement}
版本A输出:{output_old}
版本B输出:{output_new}
请对比两个输出,判断B相对A:better / same / worse,并给出简短理由。
输出JSON格式。
"""

针对双 Token 机制,如果 JWT Token 在 15 分钟的短期过期前发生泄露,这段时间内数据暴露的危险窗口期问题有没有办法解决?

双 Token 机制一般指短期访问 AccessToken(JWT,例如 15 分钟有效期)加长周期 RefreshToken,AccessToken 用来业务接口鉴权,RefreshToken 用来刷新获取新 AccessToken。AccessToken 本身是无状态 JWT,服务端不保存它,一旦泄露,在 15 分钟有效期内攻击者可以正常使用该 Token 访问接口,这就是危险窗口期。原生 JWT 设计本身无法主动作废已经签发的 AccessToken,因为服务端没有存储这份令牌,只能等待它自然过期,单纯依靠缩短过期时间只能缩小窗口,不能彻底消除风险,需要组合多套方案降低、遏制窗口期带来的危害。

第一种方案引入 AccessToken 黑名单机制,当感知 Token 泄露、用户登出、改密码时,将泄露的 jwt 的 jti 编号存入黑名单缓存,例如 Redis。每次接口鉴权解析完 JWT 之后,额外校验 jti 是否存在黑名单中,如果命中直接拒绝访问。这里要注意黑名单只保存到该 JWT 本身的过期时间,过期就可以删除缓存,避免 Redis 数据无限膨胀。该方案的代价是每一次接口请求多一次 Redis 查询,带来少量性能损耗,高并发场景可以做本地缓存 + Redis 二级缓存降低压力。缺点是完全依赖黑名单存储组件,如果 Redis 宕机,黑名单校验失效。

第二种方案绑定上下文信息,在 JWT payload 中注入客户端上下文指纹,例如客户端 UA、设备指纹、IP 模糊哈希。每次请求拿到 JWT 之后,服务端重新计算当前请求的指纹,和 Token 内部携带指纹做比对。攻击者拿到泄露 Token,在其他设备、其他 IP 发起请求,指纹不匹配直接拒绝。IP 不能直接完整比对,用户 IP 会发生网关、运营商变动,只取 IP 前两段做哈希。该手段可以阻止大部分跨设备盗用,但攻击者如果在同一台机器、同一网络环境拿到 Token,指纹不会变化,防护失效,只能作为辅助手段,不能单独解决窗口期。

第三种是风险操作二次鉴权,把接口做分级。普通查询类接口允许凭 AccessToken 直接访问;高风险接口,例如修改密码、资金操作、敏感数据导出,即使携带合法有效的 AccessToken,也强制要求额外二次校验,例如验证码、生物校验、重新输入密码。即便 AccessToken 泄露,攻击者也只能读取普通数据,无法执行高危操作,缩小泄露之后造成的业务损失,这是业务层面非常有效的兜底手段。

第四种,缩短 AccessToken 有效期,把 15 分钟下调到 5 分钟甚至更短,物理上压缩危险窗口时长。但是会提高 RefreshToken 刷新频率,增加刷新接口压力,必须配合 RefreshToken 的安全加固,RefreshToken 要做服务端存储,支持主动作废,RefreshToken 泄露可以直接在后台禁用对应记录。

第五种,检测异常访问行为做实时拦截。埋点采集用户访问行为,当出现异地访问、短时间大量接口调用、异常敏感接口访问,即使 Token 还没过期,主动触发将该 jti 加入黑名单,强制失效 Token。依靠风控系统识别异常流量,主动关闭窗口期。

需要明确没有单一方案可以完全消除窗口期,JWT 无状态本质决定它不能远程失效,生产环境一般组合:黑名单缓存 + 设备指纹校验 + 高危接口二次鉴权 + 风险行为检测共同防护。RefreshToken 不能是 JWT,应当存库支持主动撤销,很多人踩坑把 RefreshToken 也做成 JWT,泄露之后同样无法作废。

面试关键点,理解 JWT 无状态的本质矛盾:签发之后服务端没有副本,无法原生主动失效。面试加分点,区分各个方案的优缺点与适用边界,说明没有银弹,需要多层防护。记忆法采用根源‑方案记忆法 :根源是 JWT 无状态无法主动作废;五大方案:黑名单、设备指纹、高危接口二次鉴权、缩短有效期、异常风控拦截;辅助使用风险边界记忆法,记住每个方案短板,黑名单有缓存开销,指纹不能防同设备盗用。

复制代码
# jwt鉴权+黑名单校验伪代码
import jwt
import redis

redis_client = redis.Redis()

def verify_access_token(token):
    payload = jwt.decode(token, options={"verify_signature":True})
    jti = payload["jti"]
    # 校验是否在黑名单
    if redis_client.get(f"jwt_black:{jti}"):
        return None,"token已被作废"
    return payload,"ok"

了解 MySQL 事务的 ACID 四个特性吗?请解释一下。

ACID 是关系型数据库事务四个核心特性,分别是原子性 Atomicity、一致性 Consistency、隔离性 Isolation、持久性 Durability,InnoDB 存储引擎完整实现这四大特性,四个特性互相配合保障事务处理可靠。

原子性 Atomicity,指一个事务是不可分割最小执行单元,事务内部多条 SQL,要么全部执行成功提交,要么全部失败回滚,不会出现部分成功部分失败的中间状态。例如转账业务,A 扣钱、B 加钱属于同一个事务,不能出现 A 扣钱成功但是 B 加钱失败。InnoDB 依靠 undo log 回滚日志实现原子性。事务执行过程中,修改数据之前,会把修改前旧数据写入 undo log。如果事务执行过程出现异常、程序崩溃、主动 rollback,数据库读取 undo log 里面的旧版本数据,把数据恢复到事务执行之前的状态,实现回滚。事务正常提交之后,undo log 对应的记录可以被清理回收。原子性保证不会出现事务半截执行完毕的脏状态。

一致性 Consistency,一致性是事务最终要达成的结果目标,不是数据库某一个单独底层机制直接实现。一致性指事务执行前后,数据库的业务约束、数据规则始终保持合法有效,不会破坏业务完整性约束。包括主键唯一、非空约束、外键约束、业务逻辑约束,例如账户余额不能为负数。原子性、隔离性、持久性加上数据库本身约束共同保障一致性。数据库只能保证数据库层面约束,业务逻辑层面一致性需要业务代码来保证。例如转账,数据库不会自动保证余额不能负数,需要业务代码判断,这部分属于业务一致性。很多面试容易混淆,一致性是最终结果,不是依靠某一个日志文件实现。

隔离性 Isolation,多个事务并发执行的时候,事务之间互相不可见,避免并发读写带来各种问题。并发事务如果不加隔离,会出现脏读、不可重复读、幻读问题。MySQL 通过锁机制与 MVCC 多版本并发控制共同实现隔离性。数据库提供多种事务隔离级别,不同隔离级别牺牲一部分一致性换取并发性能。隔离级别越高,并发性能越差;隔离级别越低,并发能力越强,越容易出现并发问题。

持久性 Durability,事务一旦提交成功,对数据库数据的修改就是永久生效的,就算数据库断电、服务器宕机、系统崩溃,提交的数据也不会丢失。InnoDB 依靠 redo log 重做日志实现持久性。MySQL 修改数据的时候,不会直接修改磁盘上数据文件,先修改内存缓冲池中的页,同时把修改记录写入 redo log buffer。事务提交的时候,强制把 redo log buffer 刷入磁盘 redo log 文件,只要 redo log 落盘成功,事务就算提交完成。后续再后台异步把内存脏页刷回磁盘数据文件。如果此时机器断电,内存数据丢失,重启 MySQL 之后,读取磁盘上 redo log,把已经提交事务的修改重新恢复到数据页,保证已经提交的数据不丢失。

面试关键点,分清实现机制:undo log 负责原子性,redo log 负责持久性,锁 + MVCC 实现隔离性;一致性是目标,由另外三者加约束共同完成。面试加分点,讲清楚一致性和另外三者区别,很多候选人错误认为一致性由某个日志实现。记忆法采用A‑C‑I‑D 分拆记忆法 :A 原子性 undo log;C 一致性(结果目标);I 隔离性锁 + MVCC;D 持久性 redo log;辅助使用业务案例记忆法,用转账案例串联四个特性理解。

了解 MySQL 的事务隔离级别吗?

SQL 标准定义了四种事务隔离级别,MySQL InnoDB 全部支持,由低到高分别是读未提交 Read‑Uncommitted、读已提交 Read‑Committed、可重复读 Repeatable‑Read、串行化 Serializable。MySQL InnoDB 引擎默认隔离级别是可重复读,RR 级别,并且在 RR 下通过 Next‑Key Lock 临键锁解决了幻读问题,这是 MySQL 和标准 SQL 定义不一样的地方。隔离级别越高,并发性能越差,锁冲突概率越高;隔离级别越低,并发越高,越容易出现并发异常现象。

读未提交 Read‑Uncommitted,隔离级别最低。一个事务可以读到另外一个事务还没有提交的数据。会产生脏读现象。事务 A 修改数据还没 commit,事务 B 就可以读到 A 未提交修改的数据,如果 A 之后回滚,B 读到的数据就是无效脏数据。生产环境几乎不会使用这个隔离级别。

读已提交 Read‑Committed,简称 RC。一个事务只能读到其他事务已经提交完成的数据,可以避免脏读。但是会出现不可重复读。同一个事务内,同一条查询 SQL,前后两次执行,中间如果别的事务提交修改了这条数据,两次查询得到结果不一样。RC 级别 MVCC 只会生成每次查询时刻的快照,不是事务启动时刻快照。RC 隔离级别下没有临键锁,只有记录锁,不会防止幻读。互联网部分业务会使用 RC,并发性能优于 RR。

可重复读 Repeatable‑Read,简称 RR,InnoDB 默认级别。在同一个事务启动之后,第一次 select 建立读快照,整个事务内多次读取同一份数据,读取都是同一个快照版本,保证同一个事务内多次读取结果一致,解决脏读、不可重复读。标准 SQL 中 RR 仍然会出现幻读,但是 MySQL InnoDB 通过 Next‑Key 临键锁,解决了幻读问题。幻读指同一个事务内,同一个范围查询,第一次查询返回 N 条,其他事务插入新数据提交之后,再次范围查询多出记录。InnoDB RR 下,当前读(select ... for update、update、delete)会加临键锁,锁住范围,阻止其他事务插入,杜绝幻读;快照读不加锁,不会感知新插入行,但不会在同一个事务快照里面展示,业务层面看不到幻读现象。

串行化 Serializable,隔离级别最高。所有 select 语句隐式转为 select ... share mode,全部读操作加共享锁,读写互相阻塞,事务全部串行执行,完全杜绝脏读、不可重复读、幻读。并发能力极差,锁等待、死锁概率大幅上升,一般只用于数据一致性要求极高,并发很小的场景,几乎不会用于线上业务。

并发带来三类问题对应隔离级别: 脏读:可以被 RC 及以上解决 不可重复读:可以被 RR 及以上解决 幻读:标准 SQL 要 Serializable;MySQL InnoDB 的 RR 依靠临键锁解决幻读。

同时区分快照读和当前读。普通 select 属于快照读,走 MVCC 读取历史版本,不加锁;select ... for update、update、delete 属于当前读,读取最新版本,会加行锁、临键锁。RC 和 RR 的快照生成时机不同,RC 每次查询生成快照;RR 事务第一次 select 才生成快照。

面试关键点,MySQL InnoDB 默认 RR,并且在 RR 下解决幻读,这是高频考点。面试加分点,区分快照读与当前读,RC、RR 快照生成时机差异。记忆法采用级别递进记忆法 :读未提交→读已提交→可重复读→串行化;记住每个级别解决什么问题,遗留什么问题;辅助使用现象对应记忆法:脏读、不可重复读、幻读分别在哪个级别消除。

复制代码
-- 查看当前隔离级别
show variables like 'transaction_isolation';
-- 设置会话隔离级别
set session transaction isolation level read committed;

请大概讲一下 SQL 查询中的 Left Join、Right Join 和 Inner Join 的区别。

Join 用来做多表关联查询,把两张或者多张表按照关联条件合并数据集,Inner Join、Left Join、Right Join 属于最常用的三种连接方式,核心差异在于保留哪一张表全部数据,如何处理不匹配的行。

Inner Join 内连接,只返回两张表满足 on 关联条件的匹配行。左表和右表按照 on 条件匹配,只有两边都能匹配上的数据,才会出现在结果集;两边无法匹配的数据直接丢弃。inner join 可以简写为 join,不写 inner 关键字效果一样。例如用户表 user,订单表 order,user inner join order on user.id = order.user_id,只会查询出有对应订单的用户,没有订单的用户不会返回,不属于该用户的订单也不会返回。inner join 关注交集。

Left Join 左连接,以左边表 作为驱动表,保留左表全部记录,去匹配右表 on 条件。左表每一行全部出现在结果集,右表满足 on 条件就拼接右表字段;如果右表找不到匹配行,右表所有字段填充 NULL。例如 user left join order on user.id = order.user_id,所有用户都会被查出来,不管有没有订单,没有订单的用户对应的订单字段全部为 null。业务中经常会在 left join 之后加 where 判断右表某个字段 is null,用来找出左表在右表没有匹配的数据。这里有一个高频坑点:where 条件和 on 条件位置不同结果差异巨大。left join 中,on 是关联匹配条件;where 是 join 完成之后对结果集过滤。如果把右表过滤条件写在 where 里面,会把右表为 null 的行过滤掉,left join 效果退化成 inner join。过滤右表条件应当写在 on 子句中。

Right Join 右连接,和 Left Join 反过来,保留右边表全部数据,去匹配左表 on 条件。右表所有记录全部返回,左表匹配不上的字段填充 NULL。right join 在实际开发中使用频率很低,完全可以改写为 left join,只需要调换两张表的前后位置,因此很多开发优先选择 left join,减少阅读理解成本。

可以用集合关系辅助理解:Inner Join 取两张表交集;Left Join 取左表全部 + 交集;Right Join 取右表全部 + 交集。

举一个简单示例,user 表两条数据 id (1,2);order 表一条数据 user_id (1)。inner join 只会返回 id=1 一条;left join 返回 user1、user2 两条,user2 订单字段 null;right join 如果 user right join order,只会返回 order 存在的那条记录。

面试关键点,区分 on 条件与 where 条件在 left join 中的不同作用,这是高频坑。面试加分点,说明 right join 业务上很少使用,一般改写 left join。记忆法采用驱动表记忆法 :Inner 取两边匹配;Left 保留左表全部;Right 保留右表全部;辅助使用坑点记忆法:left join 右表过滤放 on,不要放 where,否则退化成 inner join。

复制代码
-- inner join
select * from user inner join `order` on user.id = `order`.user_id;
-- left join
select * from user left join `order` on user.id = `order`.user_id;
-- right join
select * from user right join `order` on user.id = `order`.user_id;

了解 SQL 的开窗函数(Window Functions)吗?

开窗函数也叫窗口函数,MySQL 从 8.0 版本开始支持开窗函数,低版本 5.7 不支持。开窗函数和普通聚合函数不一样,普通聚合函数 group by 会把多行压缩合并成一行;开窗函数不会减少原始表行数,会在原有每一行结果后面,额外增加一列窗口计算结果。开窗函数使用OVER()关键字定义窗口,窗口就是一组行集合,针对当前行的 "相关一组数据" 做计算,不会改变原始行数。

OVER 子句内部可以包含 PARTITION BY 分区、ORDER BY 排序、ROWS/RANGE 帧定义。PARTITION BY 相当于窗口的分组,把数据集切分成多个分区,窗口函数在每个分区内部独立计算,类似 group by,但是不会合并行。如果不写 PARTITION BY,整个查询结果集作为一个大窗口。ORDER BY 用于窗口内部排序,配合排名类函数;ROWS 或者 RANGE 用来定义窗口帧,限定分区内参与计算的行范围,例如当前行向前 N 行、向后 N 行。

开窗函数分为两大类,排名类窗口函数、聚合类窗口函数。

排名类窗口函数,常用于做分组内排名。 ROW_NUMBER ():分区内生成连续不重复序号,即使值相同,序号也不一样; RANK ():相同值排名相同,会跳过后续名次,例如 1,1,3; DENSE_RANK ():相同值排名相同,不跳名次,例如 1,1,2。

聚合类窗口函数,把 sum、avg、count、max、min 等普通聚合函数放在 over 前面,就变成窗口聚合函数。不是 group by 合并,每一行都返回分区内聚合结果。例如 sum (amount) over (partition by user_id),每一条订单记录,附带输出该用户全部订单总金额。

还有取值类窗口函数,LAG ()、LEAD (),LAG 取分区内当前行向上偏移 N 行的数据;LEAD 取向下偏移 N 行,常用于同比环比,拿上一条、下一条记录做对比。FIRST_VALUE、LAST_VALUE 取窗口帧第一行、最后一行的值。

常见业务场景,分组内部排名,例如每个班级学生成绩排名;求分组内汇总值做对比,每条明细带上本组总和;获取上一条、下一条记录做环比计算;求 TopN 分组数据。

开窗函数容易踩坑点,OVER 里面有 ORDER BY,默认窗口帧是 RANGE BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW,也就是从分区第一行到当前行,不是整个分区全部行,使用 LAST_VALUE 的时候很容易踩坑,拿不到分区最后一行,需要手动修改帧范围。5.7 版本没有窗口函数,只能用子查询、变量模拟,写法繁琐。开窗函数不会过滤行数,如果要取每个分组 top‑N,需要把开窗结果作为子查询,外层 where 筛选 rn<=N。

举个业务示例,订单表统计每个用户订单,同时输出该用户所有订单总额,并且对每个用户订单按金额排名。

面试关键点,对比 group by:group by 合并行数,窗口函数保留原始行数。面试加分点,讲清楚窗口帧带来 LAST_VALUE 常见坑,区分 ROW_NUMBER/RANK/DENSE_RANK 区别。记忆法采用分类记忆法 :排名函数、聚合窗口、偏移取值函数;辅助使用核心差异记忆法:group by 压缩行数,over 开窗不压缩行数。

复制代码
-- 每个用户订单,组内金额排名,附带用户总订单金额
select
    order_id,
    user_id,
    amount,
    row_number() over(partition by user_id order by amount desc) as rn,
    sum(amount) over(partition by user_id) as user_total
from `order`;

MySQL 索引在什么情况下会失效?使用 LIKE 进行模糊查询时,索引什么情况下会失效?

MySQL 索引失效本质是优化器评估后放弃走 B + 树索引,选择全表扫描,分为索引列运算、隐式类型转换、最左前缀不满足、or 条件存在非索引字段、like 通配符放在左侧、优化器成本判断、字段使用函数、order by/group by 失效等多种场景。

索引列参与运算或者函数调用,会造成索引失效。where 条件中对索引列做算术运算、函数调用,例如where date(create_time) = '2026‑01‑01',索引存储的是原始 create_time 的值,数据库无法使用函数计算后的结果去匹配索引树,只能全表扫描。正确做法是把计算挪到常量一侧,改为where create_time >= '2026‑01‑01' and create_time < '2026‑01‑02'

隐式类型转换也是高频失效场景,索引字段是字符串,传入数值,或者索引字段是数字传入字符串常量。例如 phone 字段是 varchar 索引,条件写where phone=13800138000,MySQL 会把索引列做隐式转数字,函数作用在索引列,索引失效。需要保证条件传入值和字段数据类型完全一致。

联合索引不满足最左前缀原则,联合索引按照字段顺序构建 B + 树,如果查询没有使用联合索引最左侧的字段,无法利用索引,会失效。联合索引 (a,b,c),查询条件只有 b、c,无法触发索引;只有 a,或者 a+b,或者 a+b+c 才可以命中。

or 条件中存在非索引字段,or 一侧字段没有索引,整条 where 条件索引失效。例如where id=1 or remark='xxx',id 有索引,remark 没有索引,or 会导致无法走索引。可以拆成两条 union 查询规避。

like 模糊查询的索引规则,B + 树索引前缀有序,只有通配符 % 放在字符串右侧 时可以命中索引,例如like 'abc%',可以利用索引前缀有序快速定位范围。如果百分号放在左边like '%abc'或者两边都有like '%abc%',前缀失去有序性,无法使用 B + 树索引,索引失效,触发全表扫描。注意,仅后缀通配符可以用索引,前缀、前后都通配索引失效。业务上如果需要全文检索,不要依赖 like%%,改用全文索引或者 Elasticsearch。

优化器成本评估导致索引失效,即使语法上可以走索引,当 MySQL 评估走索引回表成本高于直接全表扫描,会放弃索引。比如查询数据量占表很大比例,超过 20%‑30% 左右,回表大量随机 IO,优化器选择全表扫描。这种 explain 看到 type 为 ALL,不是语法层面失效,是优化器的成本选择。

使用 not、!=、is not null 不等于一定索引失效,很多人存在误区,不等于会走索引,但返回行数多的时候优化器会放弃;is null 同样,数据分布影响执行计划。

order by、group by 如果索引顺序不匹配,也无法使用索引排序,产生 filesort 文件排序。

面试关键点,区分语法层面失效和优化器成本选择导致不走索引;like 只有后缀 % 可使用索引。面试加分点,区分误区:!=、is not null 不代表必然索引失效。记忆法采用语法‑优化器记忆法 :函数运算、隐式转换、最左前缀丢失、左通配 like、or 无索引字段属于语法失效;大量数据返回是优化器成本选择。辅助使用 like 专项记忆:'xxx%'可用,'%xxx''%xxx%'索引失效。

复制代码
-- 可以走索引
select * from user where phone like '138%';
-- 索引失效
select * from user where phone like '%138';

消息队列在 AI Agent 系统中的作用是什么?为什么不直接通过数据库通信?

AI Agent 系统会产生大量异步任务,工具调用、文档解析、大模型推理任务、长链路思考、实验指标采集、事件通知、Skill 执行、多 Agent 编排,很多任务耗时不可预测,大模型推理、文件解析可能秒级甚至几十秒,同步接口会超时阻塞。消息队列在 Agent 体系中承担异步解耦、流量削峰、任务可靠调度、事件广播、异步结果流转、隔离长耗时任务的能力。

消息队列第一个核心作用,同步转异步,解耦 Agent 思考规划与实际任务执行。Agent 规划模块生成需要执行的任务,不阻塞等待任务完成,把任务投递消息队列,worker 消费队列去执行真实工作。例如 Auto Research 调研 Agent,规划出多个网页解析任务,直接把解析任务丢入 MQ,多个 worker 并行消费执行,规划线程可以立刻返回继续做后续推理,不会被 IO、模型调用卡住。

第二个作用流量削峰限流。Agent 批量实验、批量生成单测、批量调研场景,短时间会爆发大量任务请求,大模型 API 有 QPS 上限,如果全部直接打向模型服务,会触发限流报错。消息队列做缓冲,worker 按照模型允许的并发速度消费队列,平缓流量,避免压垮下游模型服务、工具服务。

第三个是可靠任务执行与失败重试。Agent 任务经常会遇到大模型 API 超时、网络抖动、工具临时不可用。消息队列提供重试机制,消费失败消息重新入队,配置退避重试;多次失败进入死信队列,保存失败任务,方便排查 Agent 失败路线,也支持后台人工重跑死信任务。

第四个能力,事件广播。Agent 一轮完整执行会产生大量事件:任务开始、工具调用完成、思考结束、任务失败、实验结束。一条事件可以投递到 MQ,多个下游消费:轨迹记录服务保存实验日志、评价脚本做指标统计、监控告警服务采集异常指标,各个下游互不影响。

接下来对比为什么不直接使用数据库做通信,也就是数据库轮询方案。数据库轮询是把任务写入任务表,worker 定时查询数据库捞取任务。第一点性能问题,任务量大的时候,worker 频繁 select 轮询数据库,给数据库带来大量查询压力,数据库 CPU 上升;为了避免重复消费还要做行锁,高并发下锁冲突严重。消息队列内部做消息存储,消费推拉机制,不需要轮询。

第二点重试、死信能力需要业务自己开发。数据库方案想要重试,需要自己维护重试次数、下次执行时间、死信标记字段,全部业务代码实现,开发工作量大;消息队列原生支持重试、死信队列、延迟消息。

第三点广播能力弱。数据库任务表一条记录只能被一个 worker 拿到处理,要广播事件给多个服务,需要插入多条记录,或者每个服务自己维护消费位点,实现复杂。MQ 一条消息可以被多个消费组各自消费,天然支持广播。

第四点消费位点管理麻烦。数据库方案要自己记录消费到哪一条,处理重复消费、消息丢失;消息队列原生维护 offset 位点,支持回溯重放历史消息,复现 Agent 实验轨迹。

数据库方案不是完全不能用,适合任务量很小、并发很低的小型 Agent 原型;生产级 Agent 系统,任务并发高、链路复杂,优先消息队列。但也要注意 MQ 带来新问题:消息重复消费,Agent 业务逻辑必须实现幂等;消息丢失风险,需要开启消息持久化;消息积压需要监控告警。

面试关键点,Agent 场景下核心痛点是任务耗时不确定、批量任务爆发、需要重试死信事件广播。面试加分点,能讲数据库轮询方案的优缺点,知道原型和生产环境取舍。记忆法采用MQ 功能记忆法:异步解耦、削峰限流、重试死信、事件广播;辅助对比记忆数据库轮询四大劣势:轮询压力、重试死信需要自研、广播难、位点管理复杂。

复制代码
# Agent任务投递伪代码
def submit_agent_task(task_payload):
    mq_producer.send(topic="agent_task_queue", value=task_payload)

def agent_worker():
    for msg in mq_consumer.consume(topic="agent_task_queue"):
        try:
            execute_skill_task(msg.value)
        except Exception as e:
            # 失败触发MQ重试,多次失败进入死信
            raise

Redis 在 AI Agent 系统中可能有哪些应用场景?如何设计缓存策略?

Redis 凭借高性能、TTL 过期、Hash、Set、Sorted‑Set、分布式锁、流等数据结构,在 AI Agent 工程中有非常多落地场景,覆盖会话管理、上下文缓存、任务状态、黑名单、向量检索辅助、速率限流、实验临时数据、分布式锁等。

会话与上下文缓存,Agent 多轮对话会累积大量历史上下文,不希望全部存入数据库。可以把会话 id 作为 key,hash 结构存储每一轮对话、工具返回、Agent 思考过程。设置 TTL 过期,会话一段时间不访问自动清理,减轻存储压力。注意不要把超大完整对话全部放 Redis,过长上下文做摘要后再缓存。

JWT/AccessToken 黑名单,Agent 对外鉴权,泄露的 token jti 存入 Redis,设置和 token 一致 TTL,自动过期释放,用于快速作废短期 JWT。

速率限制限流,大模型 API 有 QPS、token 速率限制,Agent 批量实验场景,用 Redis 做滑动窗口、计数器限流,防止调用模型超限报错,控制并发调用模型的数量。

分布式锁,多 worker 部署 Agent 集群,防止同一个 Agent 任务被多个 worker 重复执行;防止并发修改同一个 Skill 文件、AGENTS.md,更新 Skill 的时候加分布式锁,避免并发写导致文件损坏。

任务中间状态缓存,长流程 Agent 任务,完整轨迹存入对象存储,中间临时状态放在 Redis。例如 Auto Research 调研 Agent,子问题队列、已收集证据临时缓存在 Redis,任务完成后落库,删除临时缓存,减少数据库频繁读写。

检索缓存,同一个 query 多次检索工具,把检索结果缓存到 Redis,避免重复调用搜索工具,节约工具调用成本,降低耗时。

简单向量缓存,高频查询的 embedding 向量缓存,避免重复调用 embedding 模型接口;小规模候选集可以用 Redis sorted‑set 做简单相似度排序,大规模向量还是要向量数据库。

去重集合,Agent 自动调研,过滤重复网页链接,已访问 url 存入 Redis Set,避免重复抓取同一网页。

接下来是缓存策略设计,要区分缓存类型:热点只读缓存、临时会话缓存、任务中间状态缓存。

缓存更新策略,检索结果、embedding 这类只读数据,采用 Cache‑Aside 旁路缓存,查询先访问 Redis,未命中再调用下游工具 / 模型,拿到结果回写 Redis,设置合理 TTL。TTL 不能设置永久,防止缓存数据和真实数据源长期不一致。

会话、临时任务状态属于读写频繁的数据,采用缓存优先,异步落库。Redis 保存最新状态,定时或者任务结束后把完整轨迹持久化数据库与对象存储,Redis 设置过期时间做自动清理。

缓存穿透防护,查询不存在的 key,不要反复打下游模型、工具,对空结果也做短时间缓存,避免大量空请求穿透。

缓存击穿,热点 key 过期瞬间大量请求打到下游,使用互斥锁,热点 key 不设置 TTL 永不过期,后台线程主动刷新缓存。

缓存雪崩,大量 key 同时 TTL 过期,造成下游瞬间压力暴涨,给 TTL 加上随机偏移量,打散过期时间,不要统一过期。

缓存一致性问题,Skill、AGENTS.md 文件发生更新,不能等待缓存 TTL 过期,需要主动删除对应缓存 key,让下一次请求加载最新 Skill 内容。

还要做内存容量管控,Agent 会话、临时数据增长很快,必须设置 TTL,禁止无限量写入 Redis;区分冷热数据,冷的历史 Agent 轨迹不要常驻 Redis,存入数据库和对象存储。

面试关键点,要结合 Agent 业务场景讲 Redis 使用,而不是只讲通用 Redis 用法。面试加分点,覆盖缓存三大问题穿透击穿雪崩,以及 Skill 更新时主动失效缓存。记忆法采用场景‑策略记忆法:会话缓存、限流、分布式锁、检索缓存、去重;辅助缓存问题记忆:穿透存空值、击穿互斥锁、雪崩 TTL 加随机偏移,变更主动删缓存。

复制代码
# Cache‑Aside检索缓存伪代码
def get_search_result(query):
    cache_key = f"search_cache:{hash(query)}"
    cache_data = redis.get(cache_key)
    if cache_data is not None:
        return json.loads(cache_data)
    real_result = call_search_tool(query)
    redis.setex(cache_key, 300, json.dumps(real_result))
    return real_result

多模态大模型的具体结构是什么样的?请详细描述视觉编码器和语言模型的衔接方式。

多模态大模型(例如 LLaVA、Qwen‑VL、GPT‑4V)主流架构属于编码器‑解码器架构,分为三大组件:视觉编码器、投影层(也叫适配器、投影矩阵)、语言大模型解码器。文本输入直接送入语言模型,图像输入经过视觉编码器提取图像特征,再通过投影层做维度对齐,映射到语言模型的词嵌入空间,拼接文本 token embedding 序列,送入语言解码器做自回归生成。整套架构中语言主干部分冻结或者部分微调,大部分训练集中在视觉编码器和投影层,也有方案做全参数微调。

视觉编码器,负责把输入图片转为一系列图像特征向量。主流选用预训练视觉模型,例如 CLIP‑ViT、SigLIP ViT。ViT 视觉 Transformer 把图片切分为固定大小 patch 块,例如 14×14 像素 patch,每张图片切出 N 个 patch,每个 patch 经过线性投影变成 patch embedding,加上位置编码,送入多层 Transformer Encoder,输出一系列图像 patch 特征向量。一张图片最终输出是N × vision_hidden_dim,N 是 patch 数量,由图片分辨率和 patch 尺寸决定。例如 336 分辨率,patch_size=14,得到 576 个图像 patch 向量。视觉编码器预训练阶段已经学习图像语义,物体、纹理、空间信息,不需要从零训练。部分模型支持多图输入,多张图片分别编码得到多组 patch 特征。

投影层(Projection Layer),是视觉和语言模型衔接最关键模块。视觉编码器输出向量维度vision_hidden_dim和语言模型内部 hidden_size 维度一般不相等。比如 ViT 输出 1024 维,语言模型 hidden_size 是 2048 维。投影层一般是一层或者两层全连接网络 MLP,把视觉特征向量映射到和语言模型 embedding 完全相同的维度空间。经过投影之后,每一个图像 patch 向量就等价于一个虚拟文本 token 的 embedding。部分方案使用 Q‑Former 做适配器,不是简单线性映射,用小型 Transformer 做特征聚合,把大量 patch 向量压缩成少量查询向量,减少图像 token 数量,降低后续语言模型计算开销。

序列拼接与输入构造,语言模型只能接收一维 token embedding 序列。用户输入 prompt 中会插入特殊多模态标记,例如<image>标记。解析输入的时候,遇到<image>标记,把图片经过视觉编码器 + 投影层得到一组图像虚拟 token embedding,替换掉 prompt 里面<image>占位符的 embedding;文本部分正常查表得到文本 token embedding。把文本 embedding 序列和图像虚拟 token embedding 拼接成完整输入序列,送入语言解码器。

语言解码器就是标准自回归大语言模型 Transformer Decoder,内部做多头自注意力,图像虚拟 token 和文本 token 之间互相做注意力计算,模型就可以理解图像内容和文本指令的关联,根据图像 + 文本上下文自回归输出文本回答。训练分为预训练、多模态对齐微调阶段。预训练视觉编码器一般用公开预训练权重;多模态对齐阶段,图像‑文本配对数据,冻结语言模型主干,只训练投影层与视觉编码器少量参数,学习图像特征到语言语义的映射;SFT 阶段使用指令微调数据,可放开部分参数。

还有两种变体架构,一种是统一编码器架构,文本和图像全部送入同一个 Encoder,多用于理解类任务,不适合生成;另一种是纯解码器架构,把 ViT 模块直接嵌入 Decoder 输入层,没有独立编码器,工业界使用较少。

高频踩坑点,图像 patch 数量太多,会显著增加输入序列长度,拉高显存与推理耗时,所以很多模型会用 Q‑Former 做特征压缩,减少虚拟图像 token 数目。位置编码方面,图像 patch 自带视觉位置编码;文本使用文本位置编码,两套位置编码互相独立。

面试关键点,三大组件:视觉编码器、投影适配器、语言解码器;投影层做维度对齐,图片转为虚拟 token 嵌入拼接进文本序列。面试加分点,讲清楚 Q‑Former 适配器作用,对比简单 MLP 投影。记忆法采用数据流记忆法:图片→切 patch→ViT 视觉编码器输出特征→投影层维度对齐→虚拟图像 token embedding 和文本 embedding 拼接→送入语言 Decoder;辅助使用组件记忆法:视觉编码器提取图像特征,投影层做桥梁,语言模型统一解码。

复制代码
# 多模态输入拼接伪代码
image_patch_feat = vision_encoder(image)
image_embeds = projection_mlp(image_patch_feat)
text_embeds = text_encoder(prompt_tokens)
# 替换<image>占位符位置,拼接图像embedding
input_embeds = replace_image_token(text_embeds, image_embeds)
output = llm_decoder(input_embeds)

内存溢出和内存泄露你怎么理解的?如何解决?

内存泄漏 Memory Leak,程序在运行过程中,申请的堆内存,业务逻辑已经不再使用这块内存,但是代码仍然持有该对象的引用,垃圾回收器 GC 无法回收这部分内存。这部分内存永久占用,无法释放。随着程序长时间运行,泄漏的内存不断累积,可用内存持续下降,内存占用持续走高,最终会间接引发内存溢出。内存泄漏是缓慢、渐进式问题,短时间压测不一定复现,往往需要长时间运行才能暴露。

常见内存泄漏场景,Java 中静态集合类长期持有对象引用;集合没有及时清理不再使用的元素;ThreadLocal 使用完成没有 remove,线程池线程复用导致对象引用残留;内部类匿名内部类持有外部类引用;流资源关闭失败;缓存没有设置淘汰策略,无限往缓存存入对象。Python 场景,全局列表无限 append 对象没有清理;循环引用在老版本 GC 处理不及时;长生命周期对象持有短生命周期对象引用;第三方 C 扩展库内存泄漏,Python GC 无法接管 C 层内存。

内存溢出 OOM Out‑Of‑Memory,指程序申请内存的时候,没有足够可用内存满足本次内存申请,直接抛出 OOM 错误,程序崩溃。内存溢出不一定是内存泄漏导致。内存泄漏长期累积可以造成 OOM;但也可以短时间直接 OOM,例如一次性加载超大文件到内存;一次性创建海量对象;数组申请超大长度;缓存一次性灌入巨量数据。泄漏是 "慢慢丢内存",溢出是 "内存不够直接报错",泄漏是原因之一,溢出是最终现象。

两者核心区别:内存泄漏是对象无用却无法回收,属于逻辑 bug;内存溢出是可用内存耗尽,属于结果现象。泄漏可以诱发溢出,但溢出不一定来自泄漏。

排查与解决思路,首先区分是内存泄漏还是单纯内存使用过高。做内存快照 dump,对比多个时间点内存快照。程序启动之后,第一次 dump;运行一段时间,做第二次 dump;再运行一段时间第三次 dump。对比快照,观察哪些对象数量持续不断上涨,明明业务逻辑已经不再使用,对象实例还在持续增多,这就是内存泄漏点。如果只是瞬时大量对象,用完可以正常 GC 回收,属于内存使用量高,不属于泄漏。

内存泄漏的解决手段,找到无意的长生命周期引用,主动切断引用。静态集合及时清理无用元素;ThreadLocal 用完执行 remove;线程池避免持有任务对象引用;缓存增加 LRU 淘汰策略、设置最大容量;关闭文件、网络流等资源;避免不必要的全局对象。C/C++ 场景 malloc 之后忘记 free 会直接内存泄漏。

内存溢出处理,如果是泄漏引起,修复泄漏代码。如果不是泄漏,短时间内存打满:优化数据读取逻辑,不要一次性全部加载大文件到大内存,改为流式分块读取;限制集合最大存储容量;调大 JVM 堆内存或者容器内存限制,但调大内存只是缓解,不能修复逻辑问题;使用对象池复用对象,减少频繁创建销毁;使用磁盘、外部存储分担内存压力。

相关推荐
枫彩1 小时前
WorkBuddy 做A股自动复盘实战:数据校验、工具调用与定时报告
大数据·网络·数据库·a股·mcp·workbuddy
其实防守也摸鱼1 小时前
CTF 入门到进阶:Web、逆向、密码学与 Pwn 实战解题指南
数据库·安全·自动化·github·copilot
晓子文集1 小时前
Tushare接口文档:每日停复牌信息(suspend_d)
大数据·数据库·金融·金融数据·量化投资
SamChan901 小时前
用PostgreSQL+pgvector构建PDF翻译记忆库:向量相似度检索+增量更新实战
数据库·python·ai·postgresql·pdf·wpf
裕晟资质规划2 小时前
首次办理军工保密资质:咨询服务选择要点与全流程交付清单(评估框架)
大数据·运维·服务器·网络·数据库·经验分享
Data_Journal10 小时前
用于网页抓取的 Node-unblocker
大数据·开发语言·数据库·python·scrapy
涤生大数据10 小时前
一次“没有运行日志”的DolphinScheduler任务失败排查
大数据·数据库·人工智能·状态模式
专注API从业者10 小时前
告别人工盯品!借助 Open Claw 搭建电商商品自动化监控与数据分析系统(完整可运行源码)
大数据·运维·数据库·数据分析·自动化
viskaz11 小时前
数据库迁移流程学习
数据库