ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

基于Agent与Redis的智能面试陪练系统实战:架构设计与工程避坑

2026/9/30 10:09:41 拓冰建站 浏览量
基于Agent与Redis的智能面试陪练系统实战:架构设计与工程避坑 1. 项目缘起与整体设计思路1.1 为什么我要啃下这个 Agent 项目《码上面试》这个项目名字本身就带着很强的目的性——它瞄准的是技术面试这个场景用 Agent 的方式去解决面试准备和模拟面试中的痛点。我最初注意到它是因为在刷 Java 面试八股文和算法工程师面试题的时候发现一个很现实的问题市面上的面试题库要么是静态的 PDF要么是简单的关键词匹配问答根本没法模拟真实面试官那种“追问”和“根据你的回答动态调整问题”的能力。而 Agent 架构恰好能补上这块短板。这个项目的核心定位我理解下来是三个关键词Agent 编排、LLM 驱动、面试场景落地。它不是那种只跑个 demo 就完事的玩具项目而是把 Agent 的规划、记忆、工具调用这些能力真正塞进了一个面试对话的完整链路里。适合谁来参考我觉得有三类人一是正在准备 Java 面试、算法工程师面试想找个能陪练的工具的求职者二是想学 Agent 开发但苦于找不到一个完整业务场景来练手的开发者三是想了解 LLM 应用怎么和 Redis 这类中间件配合做状态管理的后端同学。我自己的背景是后端开发平时 Java 面试八股文背了不少但真到模拟面试环节总是卡壳。所以这个项目对我来说既是一个学习 Agent 框架的入口也是一个能直接用的面试陪练工具。下面我就把整个学习过程拆开从设计思路到实操细节再到踩过的坑完整记录一遍。1.2 整体架构的取舍逻辑拿到一个 Agent 项目我习惯先看它的分层。这个项目大致分四层接入层负责接收用户的面试请求Agent 编排层负责调度 LLM 和工具记忆层用 Redis 做会话状态和上下文管理知识层则是面试题库和 LLM 的知识库。这个分层不是拍脑袋定的背后有很实际的考量。先说为什么用 Redis 而不是直接塞内存。面试对话有个特点单次会话可能持续几十分钟中间用户可能刷新页面、切换设备甚至隔天回来继续。如果状态只放进程内存一重启就全丢了。Redis 的过期策略和持久化能力刚好匹配这个需求而且它的数据结构天然适合存对话历史——用 List 存消息队列用 Hash 存会话元数据用 Set 存用户标签操作起来很顺手。我试过用本地文件存读写延迟在并发上来后直接爆炸换成 Redis 之后单次读写稳定在毫秒级。再说 LLM 的选型。项目里没有绑定某一家模型而是做了抽象层这点很关键。因为面试场景对模型的指令遵循能力要求高不同模型在追问逻辑上的表现差异很大。我实测下来有些模型在“根据候选人回答生成下一个问题”这个任务上会跑偏要么重复问要么问得太泛。所以抽象层让切换模型变得容易这点在设计上就留了余地。Agent 编排层是整个项目的心脏。它要决定什么时候调用 LLM 生成问题什么时候查知识库什么时候更新记忆。这里的核心是状态机的思路面试有明确的阶段——开场、技术追问、算法题、反问环节、结束。每个阶段 Agent 的行为不一样用状态机来约束比让 LLM 自由发挥要稳得多。我踩过的坑是早期版本让 LLM 自己决定下一步结果它经常在技术追问阶段突然跳到算法题节奏全乱。后来加了状态约束体验才正常。2. 核心细节解析与实操要点2.1 Agent 的规划能力怎么落地Agent 和普通 LLM 调用最大的区别就是它有“规划”能力。在这个项目里规划体现在两个层面宏观的面试流程规划和微观的单轮对话规划。宏观规划靠的是前面说的状态机。我把面试流程定义成一个有向图节点是阶段边是转移条件。比如“技术追问”阶段如果候选人连续两轮回答质量高就转移到“算法题”阶段如果回答质量低就停留在原阶段继续追问。这个质量评估不是让 LLM 随便打分而是用了一个简单的规则回答长度、是否包含关键术语、是否有代码片段。规则虽然糙但比 LLM 打分稳定得多。我试过让 LLM 打分同一个回答两次打分能差 2 分完全没法用。微观规划则是单轮对话内的。Agent 收到用户回答后要先判断意图——是在回答问题还是在反问还是在请求提示。然后决定动作生成下一个问题、给出提示、还是结束当前话题。这里用到了 LLM 的 function calling 能力把“生成问题”“查知识库”“结束话题”定义成工具让 LLM 来选择调用哪个。实测下来function calling 的准确率比纯 prompt 高不少尤其是在多轮对话里模型不容易忘记自己有哪些工具可用。提示function calling 的工具描述要写得非常具体包括什么时候用、什么时候不用。我一开始只写了工具名和参数模型经常乱调。后来在描述里加了“仅当候选人明确表示不会时调用提示工具”准确率明显提升。2.2 Redis 在会话管理中的具体用法Redis 在这个项目里不是简单当缓存用而是承担了会话状态的核心存储。我梳理了一下主要用了这几种数据结构数据结构用途关键操作List存储对话历史LPUSH 追加消息LRANGE 读取最近 N 条Hash存储会话元数据HSET 更新阶段、分数HGETALL 读取全量Set存储用户标签SADD 添加标签SMEMBERS 读取String存储当前问题SET 覆盖GET 读取对话历史用 List 存是因为面试对话是典型的追加型数据而且读取时通常只需要最近几轮。用 LPUSH 追加然后用 LRANGE 取最近 10 条性能很好。我试过用 Hash 存历史每次都要序列化整个对象数据量一大就慢。List 的分段读取优势很明显。会话元数据用 Hash是因为字段多且需要单独更新。比如面试阶段变了只需要 HSET 一个字段不用读写整个对象。用户标签用 Set是为了做去重和快速判断比如判断用户是不是“Java 方向”直接 SISMEMBER 就行。这里有个细节过期时间怎么设。我一开始给所有 key 都设了 24 小时过期结果用户第二天回来发现会话没了。后来改成对话历史 7 天过期元数据 30 天过期当前问题 1 小时过期。这样既不会占太多内存又能让用户隔天继续。Redis 的内存占用我监控过单会话大概 50KB 左右一千个并发会话也就 50MB完全扛得住。2.3 LLM 提示词的设计门道提示词是这个项目里最“玄学”的部分但也有一些可复现的经验。我把提示词分成三类系统提示词、阶段提示词、工具提示词。系统提示词定义 Agent 的角色和底线。比如“你是一名资深 Java 面试官擅长追问技术细节不轻易给出答案”。这里的关键是约束要具体不能只说“你要专业”而要说“当候选人回答模糊时你要追问具体实现”。我对比过加了具体约束后模型跑偏的概率下降很多。阶段提示词是每个面试阶段单独用的。比如算法题阶段提示词里会包含题目描述、输入输出示例、以及“不要直接给答案先引导候选人思考”。这种分阶段的设计比一个万能提示词效果好得多。我试过用一个提示词覆盖所有阶段结果模型在算法题阶段还在问 Java 八股完全串了。工具提示词是给 function calling 用的前面提过描述要具体。这里补充一点工具的参数描述也要写清楚。比如“问题难度”这个参数要说明“1 表示简单5 表示困难”不然模型可能传个字符串进来。注意提示词里的示例非常重要。我在每个阶段提示词里都放了一到两个“好问题”和“坏问题”的示例模型生成的问题质量明显更稳定。示例不用多但要有代表性。3. 实操过程与核心环节实现3.1 环境准备与依赖安装动手之前先把环境搭好。这个项目依赖的东西不多但版本要对上不然会出各种奇怪的错。JDK 17项目用了 Spring Boot 3.x最低要求 JDK 17。我试过用 JDK 11启动直接报错。Redis 7.x用 Docker 装最省事命令是docker run -d --name redis -p 6379:6379 redis:7。如果想装主从可以再加一个从节点但学习阶段单节点够了。Maven 3.8用来拉依赖和构建。LLM API Key项目支持多家模型我用的是一家国内厂商的配置在application.yml里。依赖装好后先跑一下 Redis 的连接测试。我习惯用redis-cli ping返回 PONG 就说明通了。然后启动项目看到控制台输出“Agent 初始化完成”就算成功。这里有个小坑Redis 的序列化配置。项目默认用 JDK 序列化存进去的是二进制用 Redis Desktop Manager 看是一堆乱码。我改成了 String 序列化虽然占空间大一点但调试方便太多。改的地方是RedisTemplate的配置类把keySerializer和valueSerializer都换成StringRedisSerializer。3.2 面试会话的完整链路走一遍环境好了走一遍完整链路。我模拟一个 Java 面试场景从开场到结束看看 Agent 每一步做了什么。第一步创建会话。用户发起请求后端生成一个 sessionId在 Redis 里初始化三个 keysession:{id}:history空 List、session:{id}:metaHash包含阶段开场、分数0、session:{id}:currentString空。然后 Agent 返回开场白“你好我是今天的面试官我们先从 Java 基础开始。”第二步用户回答。用户输入“Java 中 HashMap 的底层结构是数组加链表加红黑树”。后端先把这条消息 LPUSH 到 history然后调用 Agent 编排层。第三步Agent 处理。编排层先读 meta发现阶段是“技术追问”于是加载对应的阶段提示词。然后把最近 10 条对话历史拼进 prompt调用 LLM。LLM 返回一个 function call选择“生成问题”工具参数是难度3、方向Java 集合。编排层执行工具生成下一个问题“那你说说红黑树在什么情况下会退化成链表”然后把这个问题 SET 到 current并 LPUSH 到 history。第四步循环。用户继续回答重复第三步直到阶段转移到“算法题”或“结束”。第五步结束。当用户说“我没有问题了”或者 Agent 判断面试完成就把 meta 里的阶段改成“结束”返回总结语。整个链路我实测下来单轮响应时间在 1.5 到 3 秒之间主要耗时在 LLM 调用。Redis 的读写基本可以忽略不计。如果觉得慢可以开流式输出让用户先看到部分内容。3.3 关键参数的计算与选择项目里有几个参数需要自己调我把我调的过程记录一下。对话历史保留轮数默认是 10 轮。我试过 5 轮模型经常忘记前面的上下文追问会重复试过 20 轮prompt 太长响应变慢而且费用上去了。10 轮是个平衡点大概对应 2000 到 3000 token大部分模型都能扛住。Redis 过期时间前面提过历史 7 天元数据 30 天。这个是根据用户行为定的。我观察下来用户平均 3 天内会回来继续7 天覆盖了 90% 的回头场景。LLM 温度面试场景不需要太有创意温度设 0.3 比较合适。我试过 0.7模型生成的问题太发散有时候会问一些和岗位无关的。0.3 的时候问题更聚焦。最大 token 数单次响应限制在 500 token。因为面试问题通常不长500 够用还能防止模型啰嗦。如果模型返回被截断说明提示词需要精简。提示这些参数没有绝对的最优值要根据你的模型和用户群体调。我的建议是先按默认跑观察一周的日志再针对性调整。4. 常见问题与排查技巧实录4.1 踩过的坑与解决方案问题一Agent 不按阶段走乱跳。表现是技术追问阶段突然问算法题。排查下来是状态机的转移条件写得太松LLM 一判断“回答质量高”就转移。解决方法是加了一个硬约束每个阶段至少问 3 轮才能转移。改完之后节奏正常了。问题二Redis 连接超时。表现是偶尔报RedisConnectionFailureException。查了日志发现是连接池配置太小默认只有 8 个连接并发一上来就不够。改成最大 50 个连接问题消失。配置项是spring.redis.lettuce.pool.max-active。问题三LLM 返回格式不对。表现是 function call 的参数解析失败。原因是模型有时候返回的 JSON 里带了多余的空格或换行。解决方法是加了一层容错解析先 trim 再 parse失败就重试一次。问题四对话历史太长导致响应慢。表现是面试进行到 20 轮后响应时间从 2 秒涨到 8 秒。原因是每次都把全部历史拼进 prompt。解决方法是只取最近 10 轮更早的历史做摘要压缩。摘要用 LLM 生成一句话概括之前聊了什么。问题五用户刷新页面后会话丢失。表现是用户刷新后 Agent 不认识他了。排查发现是 sessionId 存在前端内存里刷新就没了。解决方法是把 sessionId 存到 localStorage刷新后重新带上。4.2 常见问题速查表问题现象可能原因排查方法解决方案Agent 乱跳阶段转移条件太松看日志里的阶段变化加最小轮数约束Redis 连接超时连接池太小看异常堆栈调大 max-activefunction call 解析失败返回格式不规范打印原始返回加容错解析和重试响应越来越慢历史太长看 prompt token 数限制轮数加摘要刷新后会话丢失sessionId 没持久化看前端存储存 localStorage模型重复问问题历史没拼对看 prompt 内容检查历史读取逻辑提示词不生效约束太模糊对比输出加具体约束和示例4.3 独家避坑技巧第一个技巧日志要打全。我在 Agent 编排层的每个关键节点都加了日志包括读到的历史、拼好的 prompt、LLM 的原始返回、解析后的结果。出问题的时候一看日志就知道是哪一步错了。我见过很多同学只打一个“调用失败”然后到处猜效率太低。第二个技巧用 Redis 的 monitor 命令调试。redis-cli monitor能实时看到所有 Redis 操作排查 key 读写问题特别有用。我一开始怀疑历史没存进去用 monitor 一看发现是 key 名拼错了少了冒号。第三个技巧LLM 调用要加超时和重试。网络抖动是常态不加超时的话一个请求卡住会拖垮整个线程池。我设的是 10 秒超时失败重试 2 次重试间隔 1 秒。这样既不会等太久又能扛住偶发失败。第四个技巧面试题库要预热。项目启动时我会把常用的面试题加载到 Redis 里用 Set 存题目 ID用 Hash 存题目详情。这样 Agent 查题的时候直接读 Redis不用每次都查数据库。预热大概花 2 秒但后续每次查题能省 50 毫秒。第五个技巧用户输入要做清洗。有些用户会输入很长的文本或者带特殊字符。我在入口加了一层清洗截断到 500 字去掉控制字符。不然这些内容拼进 prompt 会干扰模型甚至导致解析失败。5. 后续扩展与个人体会这个项目跑通之后我最大的体会是Agent 项目的难点不在 LLM而在工程。LLM 调用本身很简单但怎么管理状态、怎么控制流程、怎么处理异常这些才是决定项目能不能用的关键。我见过太多 demo 跑得飞起一上真实场景就崩问题都出在工程细节上。后续我打算往两个方向扩展。一是多模态让用户能上传代码截图Agent 识别后针对性提问。这个需要接 OCR 和代码理解模型工程量不小。二是多 Agent 协作一个 Agent 负责技术追问一个负责算法题一个负责行为面试它们之间通过 Redis 共享状态。这样每个 Agent 的提示词可以更专注效果应该更好。另外Redis 的用法还可以再优化。目前是单节点如果要做高可用可以上主从加哨兵。我试过用 Docker 装 Redis 主从配置不复杂但要注意主从同步延迟。面试场景对一致性要求不高延迟个几百毫秒可以接受。最后分享一个小技巧面试结束后的复盘报告。我在会话结束时让 LLM 根据整个对话历史生成一份复盘包括表现好的地方、需要改进的地方、推荐的学习资料。用户反馈这个功能很实用比单纯打分有价值得多。实现上就是把 history 全部读出来拼一个总结提示词调一次 LLM。成本不高但体验提升明显。这个项目我还在持续迭代后面有新的心得再记录。如果你也在做 Agent 相关的项目欢迎交流尤其是状态管理和提示词工程这块我觉得还有很多可以挖的细节。