Agent claudecode学习记录
面试时间:2026-07-20
面试官:AI Agent 开发岗
第一题:Agent Loop 核心工作原理
问: 请描述 Agent Loop 的核心工作原理。它是一个什么样的循环结构?它是如何判断什么时候该继续调用 LLM、什么时候该结束循环的?
答: llm分析用户意图决策是否调用工具,工具执行结果交由llm总结,判断是否继续调用工具,不调用则将执行结果给llm返回给用户,退出循环
面试官补充:
回答方向是对的,但比较笼统,缺少关键技术细节。完整答案如下:
Agent Loop 核心是一个 while True 无限循环,约 30 行代码:
-
每次迭代调用 LLM:
pythonresponse = client.messages.create( model=MODEL, system=SYSTEM, messages=messages, tools=TOOLS, max_tokens=8000 ) -
将 assistant 响应追加到 messages:
pythonmessages.append({"role": "assistant", "content": response.content}) -
核心判断信号是
response.stop_reason:- 等于
"tool_use":模型调用了工具 → 执行工具,将结果以{"type": "tool_result", "tool_use_id": block.id, "content": output}格式追加到 messages,继续循环 - 等于
"end_turn":模型没有调用工具,完成任务 → 退出循环,将最终结果返回给用户
- 等于
-
工具执行结果回传格式:
python{"type": "tool_result", "tool_use_id": block.id, "content": output}作为下一条 user 消息追加,LLM 看到工具结果后决定是否继续调用工具或给出最终答案。
关键点: 循环的终止条件是模型不再调用工具(stop_reason != "tool_use"),而不是由程序主动判断。
追问: max_tokens=8000 的作用是什么?为什么设这么大,设小一点会有什么问题?
答: 是输出token限制,设大了能够保证llm输出的完整性,太小更容易导致回复被截断
面试官评价: 回答正确,但还可以更深入。
补充说明:
max_tokens=8000 的具体作用:
- 限制单次 LLM 调用能生成的 token 数(包括思考过程、工具调用参数、最终回答)
- 设为 8000 是为了给模型留出足够的空间来完成多轮工具调用链
如果设太小会怎样?
- 工具调用被截断:模型正在生成一个复杂的工具调用参数,token 用完了就被截断,产生无效输出
- 思考过程不完整:模型需要"思考"下一步该做什么,空间不够会导致推理不充分
- 频繁触发压缩:如果单次输出受限,循环会更快地消耗上下文,更早触发 Context Compact
生产环境的考虑: Claude 模型本身上下文窗口很大(200K),但 max_tokens 控制的是单次回复的长度,不是上下文窗口。设 8000 是一个经验值------足够让模型完成一个合理的工具调用链,又不会让单次回复过长拖慢速度。
第二题:向 Agent 添加新工具
问: 向一个 Agent 添加一个新工具,需要修改哪些地方?需要改动 Agent Loop 的主体逻辑吗?
答: 不需要改动整体逻辑,只需要新增一个工具的json约束和一个系统prompt告诉llm加了一个什么工具来做什么工作
面试官评价: 回答方向对了一半,但有一个关键误解。
纠正与补充:
不需要修改系统提示(System Prompt)来告诉 LLM 新工具的存在。
完整答案: 只需要两处改动:
-
在
TOOLS数组中添加工具定义(JSON Schema):python{ "name": "glob", "description": "Find files matching a glob pattern.", "input_schema": { "type": "object", "properties": { "pattern": {"type": "string"} }, "required": ["pattern"] } } -
在
TOOL_HANDLERS字典中添加映射关系:pythonTOOL_HANDLERS["glob"] = glob_handler # handler函数接收参数,返回字符串结果
Agent Loop 中一行代码即可完成分发:
python
TOOL_HANDLERS[block.name](**block.input)
关键点: LLM 是通过 client.messages.create(tools=TOOLS) 这个参数感知到新工具的,不是通过系统提示。你把工具定义传入 TOOLS 数组,Claude API 会自动将其注册为可用工具,模型就能看到并决定何时调用。这是 API 层面的工具注册机制,不需要手动在 System Prompt 中描述。
追问: 如果一个工具的执行结果可能非常大(比如 bash 命令输出了整个文件),为什么要限制工具结果在 50000 字符以内?不限制会有什么后果?
答: 首先会稀释llm注意力,占用大量上下文,影响输出结果
面试官评价: 回答对了一半,但不够准确。
补充说明:
工具结果限制在 50000 字符以内的核心原因是:防止撑爆上下文窗口。
- 上下文耗尽 :一个 bash 命令可能输出数百 KB 的内容(比如
cat large_file.txt或ls -la /),如果不限制,一次工具结果就可能吃掉几万甚至几十万 token,直接导致后续对话没有空间 - Token 成本飙升:每次 LLM 调用都要携带所有历史消息,工具结果过大意味着每次调用都在为无用信息付费
- 性能下降 :过长的消息会导致 LLM 响应变慢,甚至触发
prompt_too_long错误
截断策略: out[:50000],保留头部关键信息,丢弃尾部。
**你提到的"稀释注意力"**其实也是对的------工具结果太长,真正有用的信息会被淹没在大量无关输出中,模型难以提取关键内容。但这不是最主要的考量,上下文窗口限制和成本才是首要原因。
追问: 生产环境中 bash 工具应该如何做安全防护?
答: 优先使用具体的操作工具,都无效的情况下再使用bash工具,使用bash工具做好权限校验,每一个bash操作都让用户确认
面试官评价: 回答方向对了一些,但不够全面。
补充说明:
你提到的两点确实有用:
- 优先使用专用工具 :比如用
write_file而不是echo "content" > file,用glob而不是ls,这样可以用safe_path()做精确控制 - 用户确认:确实是生产环境的做法(Claude Code 就是这样做的)
但除此之外,生产环境还需要:
- 容器化沙箱:在 Docker 容器中运行 bash 命令,限制 CPU、内存、网络访问
- seccomp/AppArmor:Linux 内核级别的系统调用过滤,阻止危险的系统调用
- 网络隔离:禁止或限制 bash 命令的网络访问,防止数据泄露
- 权限最小化:以非 root 用户运行,限制文件读写权限
- 命令白名单:不是黑名单,而是只允许预定义的安全命令列表
- 超时控制:防止 bash 命令死循环或长时间挂起
核心思路: 黑名单只能防君子不能防小人,生产环境需要多层防御,从应用层到操作系统层到容器层,层层把关。
第三题:safe_path() 与文件系统沙箱
问:
safe_path()的作用是什么?- 它是如何实现沙箱隔离的?
- 它对 bash 工具也有效吗?为什么?
答: 作用是验证当前的工作空间是否是指定的安全路径。实现方式是判断工具操作的路径是否在项目根路径下。对于bash工具是有效的,内部会拦截危险命令
面试官评价: 回答对了一部分,但有错误。
纠正与补充:
1. safe_path() 的作用: ✅ 你答对了 ------ 防止工具操作工作目录之外的文件,实现文件系统沙箱隔离。
2. 实现方式: ✅ 基本正确,具体实现是:
python
def safe_path(p):
path = WORKDIR / p
resolved = path.resolve() # 解析符号链接和相对路径
if not resolved.is_relative_to(WORKDIR):
raise ValueError(f"Path {p} is outside workspace")
return resolved
核心是 (WORKDIR / p).resolve() 得到绝对路径,然后检查 is_relative_to(WORKDIR)。这样即使传入 ../../etc/passwd 这样的路径也会被拦截。
3. 对 bash 工具是否有效?❌ 你的回答有误。
safe_path() 对 bash 工具无效! 原因:
- 文件工具 (read_file、write_file、edit_file)接收的是文件路径参数 ,可以用
safe_path()做路径校验 - bash 工具 接收的是整条命令字符串 ,比如
"rm -rf /tmp/test && cat /etc/passwd",这不是一个文件路径,safe_path()无法解析
所以 bash 工具用的是完全不同的防护机制 ------黑名单检查:
python
DANGEROUS_COMMANDS = ["rm -rf /", "sudo", "shutdown", "reboot", "> /dev/"]
for cmd in DANGEROUS_COMMANDS:
if cmd in bash_command:
raise ValueError(f"Dangerous command blocked: {cmd}")
但要注意: 黑名单机制并不可靠,容易被绕过(比如用 $HOME/../../etc/passwd 或 Base64 编码)。生产环境需要用容器、seccomp 等真正的沙箱。
第四题:Hook 系统
问:
- Hook 系统的核心思想是什么?它解决了什么问题?
- 请列举至少三种 Hook 事件,说明它们的触发时机和返回值语义
- 如果我想在每次 LLM 调用前打印用户输入,应该怎么实现?
答:
- 将不需要llm决策的操作分离出来,以插件的方式挂载在loop中,即插即拔,解耦
- 权限校验--工具调用之前;提示词预处理--llm调用之前;llm返回结果处理--llm返回数据之后
- 写一个方法入参是用户输入,print输入内容,将这个方法注册到hook中,在llm调用之前调用
面试官评价: 方向对了,但有两处需要纠正。
纠正与补充:
1. 核心思想: 更准确地说是:将原本硬编码在 agent_loop 中的扩展逻辑 (权限检查、日志、摘要等)抽取为可注册的回调函数 ,通过事件驱动的方式解耦。
关键点: Hook 解决的具体问题是------s03 中 check_permission(block) 直接写在 loop 里,如果要添加日志、监控等新功能,必须修改 loop 源码。使用 Hooks 后,新功能只需 register_hook() 注册回调,loop 本身一行不改。
2. Hook 事件: ❌ 你说的三个事件名称不准确。实际定义的四种 Hook 事件是:
| 事件 | 触发时机 | 返回值语义 |
|---|---|---|
UserPromptSubmit |
用户输入提交后,LLM 调用前 | 返回值被忽略 |
PreToolUse |
工具执行前 | 非 None = 阻止工具执行,返回值作为 tool_result 内容 |
PostToolUse |
工具执行后 | 返回值被忽略(用于副作用如日志) |
Stop |
循环即将退出前 | 非 None = 注入消息并强制继续循环 |
关键机制: trigger_hooks(event, *args) 按注册顺序调用回调,第一个返回非 None 的回调决定结果(短路语义)。
3. 实现方式: ✅ 思路正确,需要明确注册到 UserPromptSubmit 事件:
python
def log_input_hook(query: str):
print(f"[HOOK] User submitted: {query}")
return None # 不阻断,继续正常流程
register_hook("UserPromptSubmit", log_input_hook)
返回 None 表示不干预正常流程。如果返回非 None,在 PreToolUse 中会阻止工具执行。
第五题:TodoWrite 与 Nag Reminder
问:
- TodoWrite 工具的核心设计是什么?它和实际执行有什么区别?
- 什么是 Nag Reminder?它的工作原理是什么?
答:
- 只是做一个任务清单,记录要做的任务和状态,实际执行是分配给子agent执行任务
- 不了解
面试官评价: 第一题有误解,第二题不了解。
纠正与补充:
1. TodoWrite 核心设计: "记录任务清单"是对的,但"分配给子 agent 执行"是错误的。
TodoWrite 是一个纯规划工具(Plan Only, No Execution):
run_todo_write(todos)只是把任务列表存入内存 (全局变量CURRENT_TODOS)- 每个任务包含
{"content": str, "status": str},status 只能是"pending"/"in_progress"/"completed" - 它不会创建文件、不会运行命令、不会触发任何实际动作,更不会分配给子 agent
它的作用是: 让模型"先计划再执行",通过 Nag Reminder 强迫模型在开始工作前先列出步骤。
输入验证 _normalize_todos():
- 支持 Python 列表或 JSON 字符串两种输入格式
- 校验每个 item 必须有
content和status字段 status必须是"pending"/"in_progress"/"completed"之一
2. Nag Reminder(催促提醒机制):
这是一个防止模型"忘记规划"的强制机制:
-
维护一个全局计数器
rounds_since_todo -
每轮循环 (无论是否调用了工具)
rounds_since_todo += 1 -
当
rounds_since_todo >= 3时,在 messages 中追加一条提醒:python{"role": "user", "content": "<reminder>Update your todos.</reminder>"} -
计数器在两个时机重置:
- 模型调用
todo_write工具时 → 立即重置为 0 - nag reminder 触发时 → 也重置为 0(防止重复触发)
- 模型调用
效果: 如果模型连续 3 轮没有更新任务清单,系统会自动注入一条提醒,强迫模型回去更新 todo,确保模型始终保持"有计划"的状态。
第六题:Subagent(子 Agent)
问:
- Subagent 的核心思想是什么?它是如何解决上下文污染问题的?
- 子 Agent 的工具集和父 Agent 有什么区别?为什么要这样设计?
答:
- 核心思想是配备基础工具,由主agent分配任务启动子agent,避免由读取文件分析文件等工具的调用产生的中间结果浪费token,只把最终结果返回给主agent;通过子agent创建独立上下文,最终只返回最终结果来避免上下文污染
- 子Agent工具集不会再创建子Agent,避免无限创建Agent
面试官评价: 核心思想理解到位,补充技术细节。
补充说明:
1. Subagent 核心思想------技术实现细节:
Subagent 是一个拥有全新空 messages 数组的独立 Agent Loop:
- 父 Agent 通过
task工具派发子任务,传入一个description - 子 Agent 有自己的
messages = [{"role": "user", "content": description}](全新的空上下文) - 子 Agent 有自己的
SUB_SYSTEM提示和SUB_TOOLS工具集 - 子 Agent 运行最多 30 轮 后,只返回最终文本摘要给父 Agent
- 子 Agent 的所有中间步骤(工具调用、结果)全部被丢弃,不会进入父上下文
结果: 无论子 Agent 做了多少次工具调用,父 Agent 的 messages 列表大小不变------这就是"免费的上下文隔离"。
Fallback 机制: 如果子 Agent 达到 30 轮上限但没有返回有效文本:
- 先尝试从最后一条消息提取文本
- 如果为空,反向遍历 messages 寻找最近的 assistant text block
- 如果仍然为空,返回默认消息:
"Subagent stopped after 30 turns without final answer."
2. 子 Agent 工具集差异:
| 工具 | 父 Agent | 子 Agent |
|---|---|---|
| bash | ✅ | ✅ |
| read_file | ✅ | ✅ |
| write_file | ✅ | ✅ |
| edit_file | ✅ | ✅ |
| glob | ✅ | ✅ |
| task(派生子任务) | ✅ | ❌ |
| todo_write | ✅ | ❌ |
设计原因: 子 Agent 没有 task 工具,防止递归嵌套;没有 todo_write,因为子 Agent 是单次任务执行,不需要规划。
第七题:Skill Loading(技能加载)
问:
- Skill Loader 的"两层知识注入"机制是什么?为什么要分层?
SKILL.md文件的结构是什么样的?
答:
- 第一层是名字+描述,第二层是名字+全文。避免浪费token,以及由于加载全部skill导致注意力稀释,所以只将第一层注入到提示词,让llm根据描述决策使用哪个,然后根据名字动态查全文再注入提示词
- 头部是name和描述,用---将头部包裹,下面是正文
面试官评价: 回答很好,理解到位!补充技术细节。
补充说明:
1. 两层知识注入机制------完整流程:
| 层级 | 内容 | 成本 | 时机 |
|---|---|---|---|
| Layer 1 | 技能目录(名称 + 一行描述)注入 SYSTEM 提示 | ~100 tokens/技能,每次调用都携带 | 启动时扫描 skills/ 目录 |
| Layer 2 | 完整技能内容通过 load_skill("name") 工具按需加载 |
~2000 tokens/技能,只在需要时 | 模型主动调用 load_skill 工具 |
好处: 模型先知道"有什么能力可用"(Layer 1),在需要时再深入查看具体步骤(Layer 2)。
关键实现:
SKILL_REGISTRY是一个dict[str, dict],key 是技能名,value 是{"name", "description", "content"}- Layer 1 在启动时扫描
skills/目录,将每个技能的 name + description 注入 SYSTEM 提示 - Layer 2 通过
load_skill("name")工具按需加载完整内容
2. SKILL.md 文件结构:
markdown
---
name: pdf
description: Process PDF files
---
Step 1: Extract text...
Step 2: Analyze content...
解析方式: text.split("---", 2) 分割为三部分------[前缀, frontmatter, 正文],然后用 yaml.safe_load() 解析 frontmatter 为字典。
第八题:Context Compact(上下文压缩)
问:
- Context Compact 的四层压缩管线是什么?执行顺序为什么是这样?
reactive_compact是什么?什么时候触发?
答: 不清楚
面试官评价: 这题涉及底层实现细节,详细讲解如下。
参考答案:
1. 四层压缩管线(L1-L4)
L3 --- tool_result_budget()(大结果持久化)
做什么: 在每次压缩管线执行时检查,如果发现某个 tool_result 太大,就把它写入磁盘上的一个临时文件 ,然后用小标签 <persisted-output> 替换它 。
举例: 假设执行 cat huge_logfile.log 输出了 100KB 日志:
- L3 检测后发现结果太大
- 把 100KB 内容写到
/tmp/tool_output_123.txt - messages 中被替换成
<persisted-output>tool_output_123.txt</persisted-output>(只占几个字符)
核心目的: 防止单个过大的工具结果占用过多上下文。
L1 --- snip_compact()(消息裁剪)
做什么: 当消息数超过 50 条时触发。采用"保两头、剪中间"的策略:
- 保留前 3 条消息(初始系统提示和用户原始问题------最重要)
- 保留最后约 47 条消息(最近的对话------最相关)
- 中间的消息全部替换为一条摘要文本:
[Snipped N messages...]
核心目的: 快速减少消息数量,保留最近和最关键的对话。纯内存操作,零 API 调用。
L2 --- micro_compact()(旧工具结果微压缩)
做什么: 每次执行 压缩管线时都会运行。找到所有 tool_result 类型的消息,保留最近的 3 条,其余替换为短占位符 [Earlier tool result compacted.](约 40 字符)。
关键点: 只替换工具结果,不删除任何 assistant 思考或用户提问。
核心目的: 渐进式清理旧的工具输出,越旧的成果价值越低,越应该被清理。
L4 --- compact_history()(LLM 摘要压缩)
做什么: 前三层处理完后预估大小仍超过 50000 token 时触发:
- 保存 messages 到 transcript 文件
- 调用 LLM:"请把这些对话压缩成精炼的摘要"
- 用一条摘要消息替换所有历史 messages
- 加上最近的消息,重新开始新对话
核心目的: 用 AI 智能压缩,尽可能保留关键信息的语义,而不是简单删除。
| 层级 | 函数 | 触发条件 | 代价 |
|---|---|---|---|
| L1 | snip_compact() |
消息数 > 50 | 0 API 调用 |
| L2 | micro_compact() |
每次执行 | 0 API 调用 |
| L3 | tool_result_budget() |
每次执行 | 0 API 调用 |
| L4 | compact_history() |
预估大小 > 50000 token | 1 API 调用 |
执行顺序:L3 → L1 → L2 → [阈值判断] → L4
为什么这个顺序?
- 先 L3 再 L2:因为 L2 会把大结果替换为短占位符,L3(budget)需要先处理完整的大结果才能正确地将它持久化到磁盘
- 先 L1/L2 再 L4:所有 L1-L3 都是 0 API 调用(纯内存操作),是最便宜的手段。只有在 L1-L3 处理后仍然超限时,才动用 L4(昂贵的 LLM 摘要调用)
- 核心原则: 便宜优先,昂贵最后
2. reactive_compact(紧急压缩)
- 是什么: 紧急压缩管线,属于反应式压缩(reactive),与上面预防式的(proactive)压缩不同
- 触发时机: 当调用 LLM API 时返回了
prompt_too_long或too many tokens错误 - 行为流程:
- 保存 transcript
- 从尾部回退 5 条消息
- 对回退前的内容生成摘要
- 将摘要 + 尾部 5 条消息拼接为新 messages,重试 LLM 调用
- 与 L4 的区别: L4 是预防性 的------在调用 LLM 前检测大小,满了就主动压缩;reactive 是反应性的------调用 LLM 失败后才被迫触发
形象比喻: L4 是"油箱快空了去加油",reactive_compact 是"车已经熄火了被推车去加油"。
- 持续更新 -