面试-Agent Loop

1 环境准备与客户端进行初始化

python 复制代码
#!/usr/bin/env python3
import os
import subprocess
from anthropic import Anthropic
from dotenv import load_dotenv

# 加载 .env 文件中的环境变量 (如 ANTHROPIC_API_KEY)
load_dotenv(override=True)

# 如果指定了自定义 base_url (例如使用本地代理或兼容接口),则移除默认验证
if os.getenv("ANTHROPIC_BASE_URL"):
    os.environ.pop("ANTHROPIC_AUTH_TOKEN", None)

# 初始化 Anthropic 客户端
# 注意:这里没有 tokenizer,SDK 会直接处理网络请求
client = Anthropic(base_url=os.getenv("ANTHROPIC_BASE_URL"))

# 获取模型名称,例如 "claude-3-5-sonnet-20241022"
MODEL = os.environ["MODEL_ID"]

# 系统提示词:设定模型的角色和行为准则
# 告诉模型它是一个代码代理,当前在哪个目录工作,多用 bash,少废话
SYSTEM = f"You are a coding agent at {os.getcwd()}. Use bash to solve tasks. Act, don't explain."

2 定义工具 (Tools)

这是 Agent 能"动手"的关键。我们不是让模型直接运行命令,而是定义一个 工具规范 (Schema)

python 复制代码
# 定义工具列表。这是一个 JSON Schema 描述。
# 模型看到后,就知道它有一个叫 "bash" 的工具可用,并且知道调用它需要传入 "command" 参数。
TOOLS = [{
    "name": "bash",
    "description": "Run a shell command.",
    "input_schema": {
        "type": "object",
        "properties": {"command": {"type": "string"}},
        "required": ["command"],
    },
}]

3 定义工具执行函数

当模型决定调用工具时,本地 Python 代码负责真正执行。

python 复制代码
def run_bash(command: str) -> str:
    # 安全过滤:防止模型生成毁灭性命令
    dangerous = ["rm -rf /", "sudo", "shutdown", "reboot", "> /dev/"]
    if any(d in command for d in dangerous):
        return "Error: Dangerous command blocked"
    try:
        # 使用 subprocess 执行 shell 命令
        # cwd=os.getcwd() 确保命令在当前脚本运行的目录下执行
        r = subprocess.run(command, shell=True, cwd=os.getcwd(),
                           capture_output=True, text=True, timeout=120)
        # 合并标准输出和标准错误
        out = (r.stdout + r.stderr).strip()
        # 限制输出长度,防止上下文爆炸
        return out[:50000] if out else "(no output)"
    except subprocess.TimeoutExpired:
        return "Error: Timeout (120s)"

4 核心循环 agent loop

python 复制代码
def agent_loop(messages: list):

    while True:
        # 【关键步骤 1:调用 API】
        # 发送消息历史给模型。
        # 注意:messages 是一个列表,里面是字典 {'role': 'user', 'content': '...'}
        # SDK 会将这个列表序列化为 JSON 发送给服务器,不需要本地 tokenizer 模板
        response = client.messages.create(
            model=MODEL, 
            system=SYSTEM, 
            messages=messages,
            tools=TOOLS,       # 把工具定义发给模型,模型才知道可以调用工具
            max_tokens=8000,
        )
        
        # 【关键步骤 2:理解响应结构】
        # 这里的 response 不是纯文本字符串!
        # 它是 Anthropic SDK 返回的一个对象 (Object)。
        # response.content 是一个列表,里面包含不同类型的"块"(Blocks)。
        # 可能是 TextBlock (文本),也可能是 ToolUseBlock (工具调用)。
        
        # 将模型的回复添加到对话历史中,保持上下文连贯
        messages.append({"role": "assistant", "content": response.content})
        
        # 【关键步骤 3:判断停止原因】
        # response.stop_reason 告诉我们要为什么停止生成。
        # 常见值:
        # 1. "end_turn": 模型说完了,不需要工具,任务结束。
        # 2. "tool_use": 模型生成了一个工具调用请求,暂停生成,等待人类/代码执行工具。
        # 3. "max_tokens": 字数用光了。
        if response.stop_reason != "tool_use":
            # 如果不是为了用工具,说明模型已经给出了最终答案,循环结束
            return
        
        # 【关键步骤 4:执行工具】
        # 如果 stop_reason 是 tool_use,说明 response.content 里包含工具调用块
        results = []
        for block in response.content:
            # 遍历内容块,寻找类型为 tool_use 的块
            if block.type == "tool_use":
                # 打印命令以便用户看到 (block.input 是模型生成的参数)
                print(f"\033[33m$ {block.input['command']}\033[0m")
                
                # 真正执行命令
                output = run_bash(block.input["command"])
                print(output[:200])
                
                # 构造工具执行结果。
                # 这个结果需要告诉模型:你刚才调用的工具 (tool_use_id),执行结果是 (content)
                results.append({
                    "type": "tool_result", 
                    "tool_use_id": block.id,
                    "content": output
                })
        
        # 【关键步骤 5:将结果喂回模型】
        # 将工具执行结果作为一条新的 "user" 消息加入历史。
        # 对模型来说,这就像是用户告诉它:"你刚才让我运行的命令,结果是这样的..."
        # 模型看到结果后,会在下一次循环中继续生成(比如分析结果,或调用下一个工具)
        messages.append({"role": "user", "content": results})
4.1 如何中断模型输出,使用工具?
  1. 现代工具调用 (Tool Calling / Structured Output):
    为了能让 Agent 可靠地调用工具,API 设计者(如 Anthropic, OpenAI)改变了返回格式。
    模型不再直接输出文本,而是输出 结构化数据 (JSON)
    当你看到 response.content 时,它实际上是一个列表,类似于这样:
json 复制代码
[
  {
    "type": "tool_use",
    "id": "toolu_01A09q90qw90lq917835lq9",
    "name": "bash",
    "input": { "command": "ls -la" }
  },
  { "type": "text", "text": "好的,我来查看文件。" },
  { "type": "tool_use", "name": "bash", "input": { "command": "ls" } }
]

4 主程序

python 复制代码
if __name__ == "__main__":
    history = [] # 保存整个对话历史
    while True:
        try:
            # 获取用户输入
            query = input("\033[36ms01 >> \033[0m")
        except (EOFError, KeyboardInterrupt):
            break
        if query.strip().lower() in ("q", "exit", ""):
            break
        
        # 将用户问题加入历史
        history.append({"role": "user", "content": query})
        
        # 进入 Agent 循环 (可能会多次调用工具,直到完成)
        agent_loop(history)
        
        # 打印模型最后的回复
        # 注意:history[-1] 是刚才 agent_loop 里最后添加的内容
        # 如果是工具调用循环结束,最后一条通常是 assistant 的文本回复
        response_content = history[-1]["content"]
        
        # 处理可能的内容格式 (列表或字符串)
        if isinstance(response_content, list):
            for block in response_content:
                # 如果块里有 text 属性,打印出来
                if hasattr(block, "text"):
                    print(block.text)
        print()

6 数据流向:

参考内容:

以下链接内容只阐述了单次调用的情况:
https://fairy-study.blog.csdn.net/article/details/159052232?spm=1001.2014.3001.5502

多次调用如何实现?

实现方式就是第四部分的 Loop ,如下所示:

  • User Input : "列出当前目录文件";
    • messages[{"role": "user", "content": "列出当前目录文件"}]
    • Tools:包含工具列表,与 messages 列表一并送入 chat_template 中。(所以如果 Tool 内容过多的话,上下文长度就会过多,模型 Prefill 阶段消耗的显存就会更多)
  • LLM Response (决定第一次 Function Call):
    • System Prompt:通过提示词的设置,使模型生成的时候思考需要用什么工具,Claude 通过一些方式使得模型的输出结构化(如 JSON),然后抽取出需要调用的工具和参数;
  • Tool Execution
    • 执行对应的工具,并将工具的结果构造为新的消息,如 {"role": "user", "content": [{"type": "tool_result", "content": "file1.txt..."}]},但本质其实还是放在 chat_template 中;
  • Feedback to LLM:
    • 构造新消息:{"role": "user", "content": [{"type": "tool_result", "content": "file1.txt..."}]},messages 变长,包含了工具返回的结果(本质就是多次问答,但是问题如果很复杂的话,会导致输入的上下文不断累积,非常大)。
  • LLM Response (2nd Call):
    • 模型看到工具结果,现在可以总结回答了。如果 response 中的 .stop_reason"end_turn",说明任务结束;
  • Loop Ends(循环截止):
    • if response.stop_reason != "tool_use" 成立,说明无需工具,返回。

7 解决了什么事情

1. 解决「纯语言模型无法动手做事」的问题

传统的 LLM 只能输出文本建议(比如告诉你「可以用 ls -la 查看文件」),但 无法实际执行 这个命令。

  • Agent Loop 把「模型决策(决定调用什么工具)」和「代码执行(真正运行命令)」结合起来,让 AI 从「只会说」变成「会动手做」。
  • 代码中通过 TOOLS 定义工具规范、run_bash 实现工具执行、agent_loop 处理「思考→执行→反馈」的闭环,完成了「语言→行动」的落地。

2. 解决「单次交互无法完成复杂任务」的问题

很多任务需要 多步操作 + 动态决策 ,比如:

需求:「找出当前目录下最新修改的 .txt 文件,并查看它的前 10 行内容」

这个任务无法一次完成,需要:

① 先执行 ls -lt *.txt 找到最新文件;

② 解析结果得到文件名;

③ 再执行 head -10 文件名 查看内容;

④ 最后总结结果。
Agent Loop 正好解决这个问题:

  • 第一次循环:模型决定调用 bash 工具执行 ls -lt *.txt,代码执行后把结果返回给模型;
  • 第二次循环:模型根据工具返回的结果,决定调用第二个 bash 命令 head -10 xxx.txt;
  • 第三次循环:模型拿到最终结果,停止工具调用,输出自然语言总结(stop_reason="end_turn")。
    如果没有这个循环,只能靠用户手动分步提问、手动执行命令,再把结果喂给模型 ------ 效率极低且不智能。而且具备 试错→调整→重试 的能力;

如何保证函数执行的安全性:

可以在代码中的 Agent Loop 还内置了安全机制,解决「模型调用危险命令」的问题(本质是在 Tool Function 中内置安全机制):

  • run_bash 函数过滤了 rm -rf /、sudo 等危险命令;
  • 限制命令执行超时(120s),避免无限阻塞;
  • 限制输出长度(50000 字符),避免上下文爆炸。
    这让 AI 执行操作时更可控,不会因为模型的误决策导致系统风险。
相关推荐
虚幻如影2 小时前
Selenium 自动化测试中 Chrome 浏览器弹出“您的连接不是私密连接”
chrome·selenium·测试工具
Surmon3 小时前
基于 Cloudflare 生态的 AI Agent 实现
前端·人工智能·架构
六月June June8 小时前
自定义调色盘组件
前端·javascript·调色盘
SY_FC9 小时前
实现一个父组件引入了子组件,跳转到其他页面,其他页面返回回来重新加载子组件函数
java·前端·javascript
糟糕好吃9 小时前
我让 AI 操作网页之后,开始不想点按钮了
前端·javascript·后端
陈天伟教授9 小时前
人工智能应用- 天文学家的助手:08. 星系定位与分类
前端·javascript·数据库·人工智能·机器学习
VaJoy9 小时前
给到夯!前端工具链新标杆 Vite Plus 初探
前端·vite
小彭努力中10 小时前
191.Vue3 + OpenLayers 实战:可控化版权信息(Attribution)详解与完整示例
前端·javascript·vue.js·#地图开发·#cesium
奇舞精选10 小时前
用去年 github 最火的 n8n 快速实现自动化推送工具
前端·agent