先是自我介绍,然后开始拷打项目
先问了我们为什么做这个agent,哪些工作是我做的。我说简历上写的工作,包括编排,主循环,以及各个部分(工具,记忆,会话等等)都是我实现的。当时是25年11月立项的。组内其他正式员工都有长期业务(语音,文本,链路质量)等等,所以这种“探索类”的事情就让我这个实习生先做了。做这个的原因是要搞老平台xxx到新平台xxxx的迁移。追问为什么要做迁移,回答一是xxx是10年前搞开源协同的时候IEG从TEG那边fork的,随着IEG更多业务的动态接入,xxx的协议和规范已经和现在的IEG业安体系渐行渐远了。二是xxx是老平台,上面的策略代码是一整篇,以RAG的形式表示策略执行的控制流,整篇代码结构很乱,难以维护,且其自带的日志打印,修改记录等也很不方便。第三是成本问题。xxx自带了TEG的信安服务,请求这个服务需要部门向TEG掏钱。IEG业安已经在xxxx上建立了信安体系的全套替代,去信安化可以大幅降低部门成本,这也是部门负责人坚持让我们组立项的原因。
先问了我的agentic workflow是怎么编排的,我就说整个workflow是orchestrator-worker架构,由中央编排器把任务分发给各个负责一部分具体任务的子agent,然后各个agent做完之后分别部署到指定的平台上去。其中,由于业务框架的限制(g3 C++框架,线上服务需要编译成动态链接库然后部署到指定平台),每个子agent都是react范式下的think-act-observe循环的agent
然后问我各个子agent是怎么管理记忆的,我说整个项目以用户维度管理跨会话记忆。记忆存储在mysql数据库里面,有用户名,记忆条目和时间戳。每条记忆正文不记录中间结果,只记录该用户在解决问题时的偏好和常用的方法。每个用户的记忆条目数量有上限,是1024条。
追问我什么时候写入,我说一次用户问题处理完全结束,退出主循环之前写入。记忆持久化部分有自己专门的子agent,用了一个相对便宜的模型(gemini 3.1 flash lite),会先读取该用户现有的所有记忆条目,然后让子agent判断本次的记忆是需要修改之前的记忆还是需要写入新记忆。如果是修改老记忆的话,修改老记忆并更新时间戳;如果是新增记忆的话,LRU淘汰已有记忆并将新记忆与时间戳写入。同一用户多个会话并发修改同一记忆时采用CAS乐观锁的方式保证一致性,即基于时间戳的check-and-swap。修改记忆的函数会传入期望的时间戳,修改之前先读取数据库内容,校验时间戳和传入的值是否相同,相同就改记忆并更新时间戳,不相同就放弃本次修改,重做下一轮think。数据库层面的一致性有mysql的底层引擎保证。某个会话修执行原子事务改数据库会话内容时数据库本身会对这一行内容上行锁,释放行锁后其他会话才能执行自己的原子事务。
然后问我怎么管理上下文的,我说think-act-observe会进行多轮循环,每轮循环开始都会拼装本轮LLM交互的message,除了system prompt就是context,即上下文。上下文包括了本轮对话的上下文,以及之前所有轮次对话的上下文。 之前轮次对话的上下文在当时的循环轮次结束之前会summarize,去掉中间过程只保留做的事情以及工具调用的中间结果。传给LLM之前会做上下文估算,分为两个部分。之前的context的token取自上一轮调用时模型返回的消耗token数,这一轮的context计算字节数n,然后参考gemini cli的计算方法,以n/4+4作为本轮消耗token数量的估算。总计算值如果小于模型提供的输入窗口乘以一个threshold(如0.8) ,就送给模型;否则就将之前轮次的context再压缩一次。
追问如果某次解决问题的过程中,agent 由于用户提供的信息不足以解决问题而结束主循环;用户在同一个会话里面的下一轮对话补充了关键信息,然后agent继续做。我是怎么解决这个问题的?我的回答是,基于之前的会话内上下文管理实现该问题。上一轮对话的context(包括做了哪些事情,有哪些重要的工具调用中间结果)被物化到该用户在此会话的上下文中,新的一轮对话只要还在这个会话内,那么首轮循环的think之前,其context对象就会把用户补充的内容和上一轮对话的context拼起来喂给LLM。
继续追问输入窗口管理问题,追问你这个既然是做策略代码(实质是lua脚本)迁移的agent,那是否会遇到超长代码,即一定超过你那个窗口乘以容限的情形?如果有的话,怎么实现的?我的回复是,这种情况很常见,比如王者荣耀局外画布就是数十万行代码的大型脚本。做这类问题的地方有两处:其一是编排器;编排器除了用大模型分析代码,还会基于老平台画布代码编写的范式,用简单的字符串匹配的方式,把不同部分的代码分发到不同的子agent,这样单个子agent持有的待迁移代码行数就会大大减少。其二是单个子agent,如果其判断需要迁移的代码量大于这个窗口,就会与workflow中的拆装重拼子agent通信,做代码行数的拆分。具体的来说就是把一整片代码切成多个固定大小的分片。拆分时为了转换后的新代码语义上连贯,会有意做不同代码块的重叠,以保证不会因为拆分来丢失语义。举一个例子,就是如果这个固定大小是1000行,那就是第一块1-1000行,第二块900-1900行。保留中间的重叠用于保证其不丢失语义,转换后由拆分重拼agent从语义上将其汇总,去掉重叠部分即可。
然后拷打了我的工具系统,先是问了我是否了解和应用了MCP协议,我说了解,然后说MCP是工具和agent之间通信的约定,其中的实体分为调用工具的MCP Server和MCP Clinent。MCP Server分为近端和远端两种,其中近端可以部署在agent(MCP CLINENT)的本机,以子进程的形式被agent主题发射,以stdio和agent通信,主要负责一些本地功能,如文件的读写和grep等;远端的MCP SERVER部署在其他的服务器上,通过stramable http与本机的agent通信。agent服务拉起时,会自动做MCP发现,分别读不同的配置文件以建立远端MCP SERVER的长连接,以及从配置文件的命令和命令参数在本地建立stdio类型mcp server的子进程。
追问,你们提供了那么多的MCP工具,agent不能一次性全加载完整的全套工具,那agent是怎么加载工具的,我回答每个MCP工具都有name和简短的description。这些信息可以拼在与LLM交互的message的非context部分,agent在解决问题的过程中会考虑是否使用工具,如果使用的话会基于description选出想调用的工具的toolname,填写到agent返回的nameList里面。observe时会基于toolname-toolSchema的映射关系,把agent想调用的工具的完整schema传到下一轮的context里面;下一轮的think,agent会基于这些schema,填写具体的调用参数,然后act部分根据agent给出的调用参数实际调用工具,并把调用结果写入本轮context。
然后是手撕环节,手撕了一个贪心+二维dp的题,我花了15分钟做出来的,做完讲了一下手撕的思路。
反问环节我为了以下问题:一是问他们的技术栈是什么,agent采用了何种架构,是开源还是自研。对方说以Java为主,自研架构没有用开源的东西 二是问他们对于这个职位看重哪些方面,回答是agent的更新迭代比较快,要求有自主学习的能力,会自己找源码找论文来探索落地方案。 然后就结束了,耗时一个半小时。今天早上看到我的流程结束了。