Agent claudecode学习记录

Agent claudecode学习记录

面试时间:2026-07-20

面试官:AI Agent 开发岗


第一题:Agent Loop 核心工作原理

问: 请描述 Agent Loop 的核心工作原理。它是一个什么样的循环结构?它是如何判断什么时候该继续调用 LLM、什么时候该结束循环的?

答: llm分析用户意图决策是否调用工具,工具执行结果交由llm总结,判断是否继续调用工具,不调用则将执行结果给llm返回给用户,退出循环

面试官补充:

回答方向是对的,但比较笼统,缺少关键技术细节。完整答案如下:

Agent Loop 核心是一个 while True 无限循环,约 30 行代码:

  1. 每次迭代调用 LLM:

    python 复制代码
    response = client.messages.create(
        model=MODEL, system=SYSTEM, messages=messages,
        tools=TOOLS, max_tokens=8000
    )
  2. 将 assistant 响应追加到 messages:

    python 复制代码
    messages.append({"role": "assistant", "content": response.content})
  3. 核心判断信号是 response.stop_reason

    • 等于 "tool_use":模型调用了工具 → 执行工具,将结果以 {"type": "tool_result", "tool_use_id": block.id, "content": output} 格式追加到 messages,继续循环
    • 等于 "end_turn":模型没有调用工具,完成任务 → 退出循环,将最终结果返回给用户
  4. 工具执行结果回传格式:

    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 是为了给模型留出足够的空间来完成多轮工具调用链

如果设太小会怎样?

  1. 工具调用被截断:模型正在生成一个复杂的工具调用参数,token 用完了就被截断,产生无效输出
  2. 思考过程不完整:模型需要"思考"下一步该做什么,空间不够会导致推理不充分
  3. 频繁触发压缩:如果单次输出受限,循环会更快地消耗上下文,更早触发 Context Compact

生产环境的考虑: Claude 模型本身上下文窗口很大(200K),但 max_tokens 控制的是单次回复的长度,不是上下文窗口。设 8000 是一个经验值------足够让模型完成一个合理的工具调用链,又不会让单次回复过长拖慢速度。


第二题:向 Agent 添加新工具

问: 向一个 Agent 添加一个新工具,需要修改哪些地方?需要改动 Agent Loop 的主体逻辑吗?

答: 不需要改动整体逻辑,只需要新增一个工具的json约束和一个系统prompt告诉llm加了一个什么工具来做什么工作

面试官评价: 回答方向对了一半,但有一个关键误解。

纠正与补充:

不需要修改系统提示(System Prompt)来告诉 LLM 新工具的存在。

完整答案: 只需要两处改动

  1. TOOLS 数组中添加工具定义(JSON Schema):

    python 复制代码
    {
        "name": "glob",
        "description": "Find files matching a glob pattern.",
        "input_schema": {
            "type": "object",
            "properties": {
                "pattern": {"type": "string"}
            },
            "required": ["pattern"]
        }
    }
  2. TOOL_HANDLERS 字典中添加映射关系:

    python 复制代码
    TOOL_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 字符以内的核心原因是:防止撑爆上下文窗口。

  1. 上下文耗尽 :一个 bash 命令可能输出数百 KB 的内容(比如 cat large_file.txtls -la /),如果不限制,一次工具结果就可能吃掉几万甚至几十万 token,直接导致后续对话没有空间
  2. Token 成本飙升:每次 LLM 调用都要携带所有历史消息,工具结果过大意味着每次调用都在为无用信息付费
  3. 性能下降 :过长的消息会导致 LLM 响应变慢,甚至触发 prompt_too_long 错误

截断策略: out[:50000],保留头部关键信息,丢弃尾部。

**你提到的"稀释注意力"**其实也是对的------工具结果太长,真正有用的信息会被淹没在大量无关输出中,模型难以提取关键内容。但这不是最主要的考量,上下文窗口限制和成本才是首要原因


追问: 生产环境中 bash 工具应该如何做安全防护?

答: 优先使用具体的操作工具,都无效的情况下再使用bash工具,使用bash工具做好权限校验,每一个bash操作都让用户确认

面试官评价: 回答方向对了一些,但不够全面。

补充说明:

你提到的两点确实有用:

  1. 优先使用专用工具 :比如用 write_file 而不是 echo "content" > file,用 glob 而不是 ls,这样可以用 safe_path() 做精确控制
  2. 用户确认:确实是生产环境的做法(Claude Code 就是这样做的)

但除此之外,生产环境还需要:

  1. 容器化沙箱:在 Docker 容器中运行 bash 命令,限制 CPU、内存、网络访问
  2. seccomp/AppArmor:Linux 内核级别的系统调用过滤,阻止危险的系统调用
  3. 网络隔离:禁止或限制 bash 命令的网络访问,防止数据泄露
  4. 权限最小化:以非 root 用户运行,限制文件读写权限
  5. 命令白名单:不是黑名单,而是只允许预定义的安全命令列表
  6. 超时控制:防止 bash 命令死循环或长时间挂起

核心思路: 黑名单只能防君子不能防小人,生产环境需要多层防御,从应用层到操作系统层到容器层,层层把关。


第三题:safe_path() 与文件系统沙箱

问:

  1. safe_path() 的作用是什么?
  2. 它是如何实现沙箱隔离的?
  3. 它对 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 系统

问:

  1. Hook 系统的核心思想是什么?它解决了什么问题?
  2. 请列举至少三种 Hook 事件,说明它们的触发时机和返回值语义
  3. 如果我想在每次 LLM 调用前打印用户输入,应该怎么实现?

答:

  1. 将不需要llm决策的操作分离出来,以插件的方式挂载在loop中,即插即拔,解耦
  2. 权限校验--工具调用之前;提示词预处理--llm调用之前;llm返回结果处理--llm返回数据之后
  3. 写一个方法入参是用户输入,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

问:

  1. TodoWrite 工具的核心设计是什么?它和实际执行有什么区别?
  2. 什么是 Nag Reminder?它的工作原理是什么?

答:

  1. 只是做一个任务清单,记录要做的任务和状态,实际执行是分配给子agent执行任务
  2. 不了解

面试官评价: 第一题有误解,第二题不了解。

纠正与补充:

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 必须有 contentstatus 字段
  • 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>"}
  • 计数器在两个时机重置:

    1. 模型调用 todo_write 工具时 → 立即重置为 0
    2. nag reminder 触发时 → 也重置为 0(防止重复触发)

效果: 如果模型连续 3 轮没有更新任务清单,系统会自动注入一条提醒,强迫模型回去更新 todo,确保模型始终保持"有计划"的状态。


第六题:Subagent(子 Agent)

问:

  1. Subagent 的核心思想是什么?它是如何解决上下文污染问题的?
  2. 子 Agent 的工具集和父 Agent 有什么区别?为什么要这样设计?

答:

  1. 核心思想是配备基础工具,由主agent分配任务启动子agent,避免由读取文件分析文件等工具的调用产生的中间结果浪费token,只把最终结果返回给主agent;通过子agent创建独立上下文,最终只返回最终结果来避免上下文污染
  2. 子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 轮上限但没有返回有效文本:

  1. 先尝试从最后一条消息提取文本
  2. 如果为空,反向遍历 messages 寻找最近的 assistant text block
  3. 如果仍然为空,返回默认消息:"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(技能加载)

问:

  1. Skill Loader 的"两层知识注入"机制是什么?为什么要分层?
  2. SKILL.md 文件的结构是什么样的?

答:

  1. 第一层是名字+描述,第二层是名字+全文。避免浪费token,以及由于加载全部skill导致注意力稀释,所以只将第一层注入到提示词,让llm根据描述决策使用哪个,然后根据名字动态查全文再注入提示词
  2. 头部是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(上下文压缩)

问:

  1. Context Compact 的四层压缩管线是什么?执行顺序为什么是这样?
  2. 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_longtoo many tokens 错误
  • 行为流程:
    1. 保存 transcript
    2. 从尾部回退 5 条消息
    3. 对回退前的内容生成摘要
    4. 将摘要 + 尾部 5 条消息拼接为新 messages,重试 LLM 调用
  • 与 L4 的区别: L4 是预防性 的------在调用 LLM 前检测大小,满了就主动压缩;reactive 是反应性的------调用 LLM 失败后才被迫触发

形象比喻: L4 是"油箱快空了去加油",reactive_compact 是"车已经熄火了被推车去加油"。


  • 持续更新 -
相关推荐
LONGZHIQIN1 小时前
TIA Portal Openness学习笔记
笔记·学习
网络工程小王1 小时前
【HCIE-AI】12.deepspeed分布式并行训练进阶版
人工智能·pytorch·分布式·学习·昇腾·deepspeed
小+不通文墨2 小时前
--no-multibyte-chars 解决OLED_data.c移植后中文乱码问题
c语言·经验分享·笔记·学习
知识分享小能手2 小时前
统计学学习教程,从入门到精通,一元线性回归 —— 知识点详解(17)
学习·算法·线性回归
青山是哪个青山2 小时前
LangChain 1.x 学习笔记(一):为什么需要 LangChain?一文彻底理解 LangChain 生态
笔记·学习·langchain
cooldream20092 小时前
AI 时代,前端工程师的“新学习路线“—— 从“学框架“到“用 AI“
前端·人工智能·学习
CappuccinoRose3 小时前
Rust学习文档(二)
开发语言·后端·学习·rust
冻感糕人~3 小时前
大模型学习指南:收藏这份AI Agent四层工程地图(小白程序员必备)
java·大数据·人工智能·学习·大模型·agent·大模型学习
曲三酒馆3 小时前
“强比命打,钱娃D写”——这8个字就是我人生的全部了![特殊字符]
学习·生活·娱乐