大家好,我是二哥呀。
如果你相信努力和过程,愿意一步一个脚印,也相信自己能够在 AI 时代获得机会,那么希望你认真读完下面这份硬核面经。
(全文确实比较肝,但保证大家能学到很多很多。系好安全带,我们粗粗发~)
老王的第一道题考查概念:“说说 LLM 和 Agent 之间有什么联系和区别。”
“先看联系,Agent 每次作出决策,背后都是 LLM 在判断。”
“是否调用工具、选择哪个工具、参数怎样填写,以及任务是否完成,都由模型决定,Agent 框架并不负责判断。”
“二者的差异体现在职责边界。LLM 本质上是无状态文本生成服务,每次调用接收上下文并输出文本;调用结束后不会保留记忆,也无法对外部世界产生改变。”
“Agent 则是以 LLM 为核心构建的执行系统,为模型补齐三种缺失能力:”
“简而言之,LLM 负责思考,Agent 则把思考结果真正执行出来。”
老王继续问:“网页版对话助手属于 Agent 吗?”
“要看它是否具备 ReAct 循环。一次输入对应一次输出的纯问答只是单次调用,不属于;当它能够自主联网检索、运行代码,并根据中间结果继续推进时,就已成为轻量级 Agent。”
“判定依据并非产品形态,而是系统有没有形成『决策、行动、观察结果、再决策』的闭环。ReAct 如何运转,下一题正好可以具体说明。”
老王点头后问:“你了解 ReAct Agent 吧?它包含哪些部分?”
“ReAct 指推理加行动(Reasoning + Acting)形成的循环。以我编写的 PaiCLI-Python 为例,可以拆成五部分,每部分都对应具体模块:”
老王又追问:“用户交给系统一个问题时,完整流程怎样运行?”
“首先完成两项准备:将匹配的 Skill 候选列表注入上下文,并检查消息总量是否需要压缩。”
“随后启动循环,将消息历史和工具清单共同发送给模型。模型以流式形式返回结果,可能直接输出文本答案,也可能提出工具调用。”
“如果是工具调用,执行器完成操作后,会把结果作为 tool 消息追加到历史,再次请求模型。模型读取工具结果后继续判断,是再次调用工具,还是给出最终答案。”
“系统有两个退出条件:模型不再调用工具时正常结束;到达 20 轮上限时则强制收尾。”
“执行阶段还有两处细节:只读工具最多允许 4 个并发运行,写操作必须严格串行,避免相互覆盖;在流式场景里,工具调用参数会分片抵达,必须依照序号拼成完整参数,再解析为 JSON 交给执行器。若过早拼接,得到的只是半截 JSON,会直接解析失败。”
老王靠向椅背问:“如果让你从零设计一个 Agent 框架,会怎样划分模块?”
“核心可以分为六层:”
“安全层经常被忽略,因此需要单独强调。Agent 能够真正执行命令,sudo、rm -rf 等危险角色都列在黑名单里,会被直接拦截。”
“写文件和执行命令必须标注危险等级,或者要求人工确认,全部执行过程还要写入审计日志。系统能力越强,越应预先明确如何约束它。”
“启动阶段完成组装:入口层把内置工具和 MCP 工具合并到同一工具注册表中,再将其交给 Agent。”
“运行时由 Agent 保存消息历史,每轮把历史传给模型层;模型返回的调用请求由工具层执行,结果再写回历史,如此循环。”
“设计上的关键决定,是要求所有模块对外只输出统一格式的流式事件。文本增量、思考增量、工具调用和用量统计全部属于事件,入口层只负责渲染,不参与业务逻辑。”
“这样做能让每一层独立替换:更换模型只需改模型层,增加工具只改注册表,调整界面只改入口层。”
老王在本子上记了一笔:“谈谈 MCP 和 tool 之间的联系与区别。”
“先从归属看差异。tool 是应用内部函数,由我编写并注册,随代码一起运行,其他应用无法直接使用。”
“MCP 将工具从应用内部拆分出来,使其成为独立服务进程,所有支持 MCP 的客户端都可以连接使用,从而解决 M 个应用与 N 个工具相互对接造成的组合爆炸。”
“二者的联系在于最终形态一致。MCP 工具连接后会被包装成内置工具的形式,注册到同一工具注册表,最后按函数调用(Function Calling)格式共同提供给模型。”
“模型既不知道,也不需要判断某个工具究竟是本地函数还是远端服务。”
“实现时我主要处理了三件事:”
“配置采用分层合并:用户目录存放全局配置,项目目录保存局部配置;服务同名时以后者覆盖前者,路径支持展开环境变量。”
“若某个 server 无法连接,就单独隔离并记录错误,不妨碍其他工具继续正常注册。”
“还有个容易被忽略的点。MCP 除了工具还有资源和提示词模板,我把它们也映射成了虚拟工具,列资源、读资源和调用普通工具走的是同一条路径,模型侧不用学新动作。”
老王翻到简历下一页:“项目里的智能问答怎样实现短期记忆和长期记忆?”
“短期记忆就是当前会话的消息历史,以列表保存,上限为 100 条,并与上下文压缩协同工作。”
“当内容达到可用输入预算的 80%时触发压缩,目标压至 55%;最近 6 条消息保持原样,更早轮次采用提取式摘要。切分边界放在用户消息处,确保工具调用及其结果成对保留。”
“长期记忆存入用户目录下的 SQLite,能跨会话使用,并按照项目路径隔离作用域。具体还有几个设计点:”
“还要明确一条边界:压缩产生的摘要仅供当前会话使用。它属于模型生成的二手信息,不能升级为长期记忆。”
“只有用户明确要求保存的事实才能进入长期记忆。一旦放宽这条边界,模型自身的转述很快就会污染记忆库。”
老王继续追问:“业界常见的三层记忆系统如何划分?每层分别存什么?”
“主流方式是按照作用域划分三层:”
| 层次 | 存什么 | 在 PaiCLI-Python 里 |
|---|---|---|
| 会话层 | 当前对话的消息 | 内存中的消息列表,最多 100 条 |
| 项目层 | 仓库规范与构建命令 | 项目根的 PAI.md,跟着 Git 走 |
| 全局层 | 跨项目个人偏好 | 用户目录下的记忆库与全局配置 |
“文件记忆还设有本地覆盖层 PAI.local.md,用于保存仅属于本机且不进入版本库的配置。加载过程同样受预算限制,单文件最多截取 6000 字符,合并后的总量不超过 16000 字符,防止记忆过度占用窗口。”
建议保存这张表,回答记忆相关问题时基本都能套用。
老王提出下一题:“你了解 Skill 的渐进式披露(progressive disclosure)机制吗?”
“了解,可以用八个字概括:索引常驻,正文按需。整个机制分为两段。”
“第一段发生在每次用户输入时,只将匹配度最高的 5 个 Skill名称和描述注入上下文。每条描述截断到 300 字符,全部索引不超过 4000 字符。此时模型只知道有哪些技能可用,正文内容一个字也不会加载。”
“第二段中,模型发现任务与某个 Skill 匹配后,主动调用加载工具,这时才读取 SKILL.md 正文,上限是 5000 字符。正文不会立即加入当前轮,而是先存入缓冲区,在下一轮随工具结果注入;缓冲区仅保留最近 3 条,避免持续累积。”
“Skill 目录也按内置、用户级、项目级划分为三层;出现同名项目时,项目级覆盖用户级,逻辑与记忆文件的分层方式相同。”
“这套机制带来的收益可以直接计算。若将 20 个 Skill 按每篇 5000 字符上限全部加载,总量就是 10 万字符;采用渐进式披露后,常驻内容只有 4000 字符索引,相差 25 倍。”
老王接着问:“系统如何匹配候选?”
“通过加权方式评分。用户明确点名某个 Skill 时直接赋予最高分;否则依据命中位置计算,名称命中权重最高,标签其次,描述最低。中文还采用二元、三元分词提高召回率。”
“通俗地说,它就是一个把检索对象由网页换成技能的微型搜索引擎。”
老王问了最后一道正式问题:“使用 AI 时,怎样保证输出内容的质量?”
“我会分三层回答,首先讲已经实现的部分。”
老王追问。“这些都是工程手段,提示词层面呢?”
“三条实践。把验收标准直接写进提示词,让模型知道什么叫合格;复杂任务要求模型先复述一遍理解再动手,提前暴露偏差;重要产出让模型对照标准自查一轮再交付。”
“我也会如实说明,通用钩子机制与结构化输出校验尚未完成,主循环同样没有自动重试。面试时最不能把没实现的功能说成已经实现——质量保证首先要保证自己的表述真实。”
老王笑了一下,把本子合上。
项目名称:PaiCLI-Python
项目简介:一款对标 Claude Code 的 Python Agent 命令行工具,具备 ReAct、计划执行、多 Agent 三种模式
技术栈:Python、asyncio、httpx、MCP、SQLite
核心职责:
面试接近结束,轮到我提问:“结合刚才的表现,能否给我一些后端和 Agent 方面的学习建议?”
老王思考片刻后给出了信息量很大的建议,兄弟实在太用心,我甚至想向他鞠躬。把原话拆开来看,就是一份完整自查清单:
这份清单里的每个项目,都可以结合一个真实项目的源码逐项学习。
过去后端面试比拼并发和中间件,如今还要理解模型、工具调用、记忆与上下文。
我们有机会置身 AI 发展的浪潮前沿,将 Agent 从概念名词变成简历中经得住追问的实际项目。挑战固然不少,可能性也同样无限。
兄弟姐妹们,加油吧。
下期再见。