找回密码
立即注册
搜索
热搜: Java Python Linux Go
发回帖 发新帖

3520

积分

0

好友

462

主题
发表于 8 小时前 | 查看: 5| 回复: 0

在大厂日爆那里看到一则消息:某大模型公司启动离职脱敏程序,核心成员必须强制签署协议,离职需提前三个月同步公司。听起来有些夸张,但确实暗示着 AI 行业正在脱离传统互联网的轨道,在薪酬、制度和规则上都走向完全不同的方向。与此同时,面试中的 AI 含量也在飙升,传统 Java 后端八股的比重持续下降,七三开的趋势已经很明显。

所以,今天就以我们实际开发的 PaiCLI(基于 Java 的 AI Coding Agent)为例,完整拆解高频 AI Agent 面试的八个核心问题,所有回答均源自实战经验。

AI Agent面试题列表:拷打项目、追问agent设计等九个问题

01、你在 Agent 项目中具体做了什么?

“先整体介绍一下,你在项目里负责了哪些模块?”
我答道:“PaiCLI 一共迭代了 21 个版本,从最原始的 ReAct 一路做到了支持 Multi-Agent 的可交付产品。我主要负责三块核心工作。”

PaiAgent 整体技术架构:四层解耦、适配器桥接、引擎可切换

第一块,Agent 引擎。 实现 ReAct 模式的主循环,完整覆盖 LLM 调用、工具执行、上下文管理全流程。每次循环都要做预算检查、上下文压缩、工具调度,这些机制都是从头设计和实现的。
第二块,LLM 多模型接入。 写了一套 OpenAI 兼容的抽象客户端,基于 OkHttp3 做 SSE 流式解析,支持智谱、DeepSeek、阶跃星辰、Kimi 四个国产模型的无缝切换。每个模型都有自己的特殊处理,比如智谱支持 prompt caching,DeepSeek 支持 1M 上下文窗口。
第三块,Tool Use 与 MCP 集成。 内置了 11 个核心工具(文件读写、命令执行、代码搜索、网页抓取等),同时支持动态接入 MCP Server 的外部工具。

PaCLI 终端界面:显示模型信息、MCP 工具数量及记忆管理

02、Agent 的循环执行是怎么设计的?

“你们的 ReAct 循环具体怎么设计的?”
“其实很简单,核心就是一个 while(true) 循环,退出条件全部交给 LLM 自己决定。”

ReAct while 循环流程图:Thought→Action→Observation,直到任务完成

每次循环做四件事:  

  • 第一步,检查预算——看 token 用量、迭代轮数,超了就强制退出;  
  • 第二步,判断上下文是否需要压缩;  
  • 第三步,把对话历史和工具定义一起发给 LLM;  
  • 第四步,解析 LLM 的返回,如果有 tool_calls 就执行工具,并把结果塞回对话历史,进入下一轮;如果没有,说明任务完成,直接返回最终答案。

Agent.java 代码片段:while(true)主循环中的预算检查与上下文压缩

追问:“退出靠 LLM 自己判断,万一把它卷进死循环呢?反复调同一个工具停不下来怎么办?”
“我们有兜底策略:跟踪两个指标——累计 token 消耗(默认上限是模型最大上下文窗口的 80%)和循环迭代次数。任何一个超标,都会直接跳出循环并返回提示给用户。”

03、是框架内置的工具调用还是 Prompt 控制输出?

“你们用 Spring AI 了吗?工具调用的逻辑是框架搞定的还是自己写的?”
“没用 Spring AI,全是自己写的。”
PaiCLI 定义了一个 LlmClient 接口作为 LLM 调用的抽象层,底下基类基于 OkHttp3 实现了 OpenAI 兼容协议的 SSE 流式请求,工具调用的流程也是自己解析的。

终端界面:用户查询ReAct模式代码,grep_code搜索相关文件

LLM 返回的 SSE 流里,tool_calls 是分片到达的——一个工具调用的 idnamearguments 可能散落在多个 SSE event 中。我们会把这些碎片存起来,流结束后再组装成完整的 ToolCall 对象。
追问:“为什么不用 Spring AI?自己造轮子不怕维护成本高吗?”
“PaiCLI 是命令行工具,启动速度很关键,Spring AI 太重了。而且现在大家都是 AI Coding,基本不存在实现不了的功能。”

04、System Prompt 是怎么设计的?

“聊聊你们的系统提示词设计,是一次性写死还是动态拼装的?”
“动态拼装,而且是分层组装。” 我们有一个 PromptAssembler,负责把系统提示词从多个 Markdown 文件里拼出来。

Prompt 分层组装结构图:从 Base 到 Output 共七层

整个提示词分成七层:  

  1. Base 层 (base.md):定义 PaiCLI 的身份、语言要求、可用工具列表与使用策略;  
  2. 性格层 (如 calm.md):定义冷静理性的风格;  
  3. 模式指令层:ReAct、Plan、Multi-Agent 各一个 Markdown,告诉模型当前工作方式;  
  4. 审批层:控制哪些工具需要用户确认才能执行;  
  5. 动态上下文层:注入记忆摘要和外部上下文源;  
  6. Skill 层:把启用的 Skill 描述注入;  
  7. 输出/协作层:上下文管理指令和协作协议。  

追问:“提示词文件放哪?用户能自定义吗?”
“有三级加载优先级:JAR 包内置的 resources/prompts/(默认);用户目录 ~/.paicli/prompts/(可覆盖默认);项目目录 .paicli/prompts/(优先级最高,适合团队定制)。这个设计参考了 Claude Code 的 CLAUDE.md 机制,让不同项目可以有不同的 Agent 行为。”

05、调用 LLM 的全过程

“从用户输入到最终返回,整个 LLM 调用过程完整说一遍。”
全流程如下:

LLM 调用完整流程图:从用户输入经过MemoryManager、Prompt组装、API调用、SSE流式到ChatResponse

第一步,构建上下文。 从短期记忆提取相关信息,拼进系统提示词的动态上下文。  

memoryManager.addUserMessage(userInput);
String memoryContext = memoryManager.buildContextForQuery(
    userInput, contextProfile.memoryContextTokens()
);
updateSystemPromptWithMemory(memoryContext);

第二步,用户消息入历史。 如果有图片引用(例如截图),会转成 base64 的 ContentPart 一并打包。
第三步,进入 ReAct 循环。 先检查是否需要压缩上下文,再检查预算。
第四步,构建 HTTP 请求。 将对话历史和工具定义序列化为 OpenAI 格式的 JSON:messages 数组里每条消息带 rolecontenttools 数组里每个工具带 namedescriptionparameters 的 JSON Schema。
第五步,发送请求并解析 SSE 流。 OkHttp3 建立长连接,逐行读取 data: 开头的事件。每个事件的 delta 可能包含三种信息:reasoning_content(思考过程)、content(回复内容)、tool_calls(工具调用)。三个字段通过 StreamRenderer 实时渲染给用户。
第六步,流结束后组装 ChatResponse。 包含完整的 content、reasoning、toolCalls 列表、token 统计。
第七步,回到 Agent 循环,决定是执行工具还是返回结果。

终端界面:用户追踪从入口到LlmClient.chat()的完整调用链

追问:“实时渲染时,思考过程和回复内容怎么区分?”
“靠 SSE 事件里的字段名。DeepSeek 和智谱的深度思考模式会在 delta 里返回 reasoning_content 字段,普通回复走 content 字段。PaiCLI 内部维护了两个状态,思考过程用折叠样式展示,不会和最终回复混在一起。”(内心 OS:这题我太熟了,流式渲染那块调了三天才搞定各种边界情况🥲)

06、Tool 什么时候发给 LLM,什么时候执行?

“工具定义什么时候发给模型的?是注册时就发,还是每次请求都发?”
“每次请求都发。” 在 ReAct 循环里调用 LLM 之前,都会从 ToolRegistry 拉一份最新工具定义:

List<LlmClient.Tool> toolDefinitions = toolRegistry.getToolDefinitions();
LlmClient.ChatResponse response = llmClient.chat(
    conversationHistory, toolDefinitions, streamRenderer
);

这样做的好处是:如果用户中途通过 /mcp 命令加载了新的 MCP Server,新工具在下一次循环就能被 LLM 感知到。

工具的注册发生在构造阶段。内置工具逐个注册,例如 read_file 包含名称、描述、参数的 JSON Schema 和执行函数:

tools.put("read_file", new Tool(
"read_file",
"读取文件内容(仅限项目根目录之内)",
    createParameters(
new Param("path", "string", "文件路径", true),
new Param("offset", "integer", "起始行号", false),
new Param("limit", "integer", "最多读取多少行", false)
    ),
    args -> {
        Path safe = pathGuard.resolveSafe(args.get("path"));
return readFileForTool(safe, args);
    }
));

MCP 工具是动态注册的。启动 MCP Server 后,registerMcpTool() 会把外部工具注册到同一个 Map,命名空间用 mcp__{server}__{tool} 格式隔离。

工具注册架构图:内置工具静态注册,MCP工具动态注册,ToolRegistry统一管理

追问:“执行呢?一个请求返回多个 tool_calls,串行还是并行?”
“看数量。单个直接在当前线程执行;多个开线程池并行跑,最多 4 个并发。”

ToolRegistry 并行执行工具代码片段:线程池处理多个工具调用

并行执行配有两道超时保护:单个命令 60 秒超时,整个工具批次 90 秒超时。若某工具超时,对应的 Future 会被取消并返回 TimedOut 状态,不影响其他工具的结果。

07、返回结果怎么知道该调用哪个 Tool?

“模型返回一堆 JSON,你怎么把它和具体的工具函数对应上?”
“关键就在 tool_calls 里的 name 字段。” LLM 返回的每个 tool_call 包含三个核心字段:id(唯一标识)、function.name(工具名称)、function.arguments(参数 JSON)。name 就是注册时用的那个名字,比如 read_fileexecute_command

Tool Call 响应结构解析:name字段关联ToolRegistry,执行对应工具

在 SSE 流式场景下,这三个字段不是一次到齐的,我们按 index 累积碎片,流结束后组装成 ToolCall 列表。然后根据 name 从 ToolRegistry 的 Map 中查找对应的执行函数,把 arguments 的 JSON 解析成参数 Map,传给工具函数执行。执行结果带上 id 打包成 tool 类型的消息塞回对话历史。

for (ToolExecutionResult toolResult : toolResults) {
    conversationHistory.add(
        LlmClient.Message.tool(toolResult.id(), toolResult.result())
    );
}

追问:“如果模型幻觉出一个不存在的工具名,比如 delete_database,怎么办?”
“找不到对应 key 时,返回错误信息‘未知工具:delete_database’。这条错误也会作为 tool 消息塞回对话历史,LLM 看到后通常会自我纠正,换个正确工具重试。另外在 base.md 系统提示词里我们也明确列出了所有可用工具,从源头减少幻觉。”(内心 OS:嘿嘿嘿,老王,这种细节追问真难不住我🤣)

08、记忆压缩方式,怎么生成摘要?

“前面提到的上下文压缩,具体怎么做?怎么生成摘要?”
“在每次 LLM 调用前触发。检查逻辑很简单:估算当前对话历史的 token 数,低于阈值(默认模型最大上下文窗口的 90%)就跳过;超过阈值则启动压缩。”

对话历史压缩示意图:Map-Reduce压缩早期历史为摘要,保留最近3轮原文

压缩不是全量处理,而是按“轮次”分割。 保留最近 3 轮对话不动,把更早的对话拿出来生成摘要。分割点必须落在 user 消息的边界,绝不能把一组 tool_call 和 tool_result 拆开,否则 LLM 会晕。
拿到待压缩消息后,调用 LLM 生成摘要,摘要提示词经过精心设计,要求保留四类关键信息:用户的核心诉求、Agent 已完成的操作、达成的共识、未解决的问题。  

private static final String SUMMARY_PROMPT = """
    请把下面的对话历史压缩成简明摘要,保留:
    1. 用户提出的关键诉求与目标
    2. Agent 已经完成的关键操作(哪些工具调用了什么、返回了什么核心结果)
    3. 已经达成的共识或结论
    4. 仍未解决的问题或待办

    不要复述每条原文,不要列举所有工具调用,不要保留无关闲聊。
    输出 1-3 段中文,不要用列表,不要加任何前缀或元描述。
    """;

摘要生成后,重建对话历史:系统提示词 → 一条 user 消息(装压缩摘要) → 一条 assistant 消息(“好的,我已了解之前的上下文,请继续。”) → 最近 3 轮原始对话。加这条 assistant 消息是为了满足 OpenAI 兼容协议中 user / assistant 消息必须交替的约定。

追问:“摘要输入有长度限制吗?万一要压几万字的对话呢?”
“限制在 6 万字符,超出部分截断取前 6 万字符送摘要。这是防止摘要请求本身撑爆 LLM 上下文。实际使用中,3 轮保留 + 90% 阈值的组合下,待压缩内容一般在 2-3 万字符,很少触及上限。”
“压缩后 token 能降多少?”
“通常压到原来的 20%-30%。比如压缩前 8 万 token,压缩后大概 2-3 万。摘要本身只有几百到一千多 token,加上保留的最近 3 轮原始对话,总 token 数大幅下降。日志里会打印压缩前后的 token 数和消息数,方便观察效果。”

记忆存储写入与读取流程:/save触发写入,每轮请求检索并注入System Prompt

如何写到简历上?

AI编程助手|Agent开发|PaiCLI  2026-03 ~ 至今

项目简介: 基于 Java 的 AI Coding Agent 命令行工具,对标 Claude Code,支持 ReAct、Plan-and-Execute、Multi-Agent 三种执行模式,接入智谱、DeepSeek、阶跃星辰、Kimi 等国产大模型。
技术栈: Java 17、OkHttp3、Jackson、JLine3、MCP Protocol
核心职责:  

  1. 设计并实现 ReAct 主循环引擎,基于 LLM 自主决策的 while(true) 架构,集成预算管理(token/迭代双维度),实现自动上下文压缩。  
  2. 基于 OkHttp3 + SSE 实现 OpenAI 兼容的多模型接入层,支持 ToolCall 分片累积解析。  
  3. 内置 11 个核心工具 + MCP 动态扩展,支持多工具并行执行(最大 4 并发)、超时保护和有序结果返回。  
  4. 实现 7 层系统提示词组装架构,支持 JAR 内置 → 用户级 → 项目级三级覆盖,实现模式切换和上下文动态注入。  
  5. 设计上下文压缩机制,按用户轮次分割、LLM 生成摘要、保留最近 3 轮原始对话,token 压缩率达 70%-80%,解决长对话场景下的上下文溢出问题。

以上内容提炼自云栈社区对 PaiCLI 项目的深度实践,如果你的 简历 里也需要补充 AI Agent 相关的项目亮点,或者正在准备相关面试,欢迎来社区进一步探讨交流。




上一篇:百万级组件告急:AntV生态npm包遭批量投毒,窃取凭证蠕虫持续扩散
下一篇:哲学硕士靠什么生存?奖助金、助教与兼职收入大揭秘
您需要登录后才可以回帖 登录 | 立即注册

手机版|小黑屋|网站地图|云栈社区 ( 苏ICP备2022046150号-2 )

GMT+8, 2026-5-20 12:47 , Processed in 0.691841 second(s), 41 queries , Gzip On.

Powered by Discuz! X3.5

© 2025-2026 云栈社区.

快速回复 返回顶部 返回列表