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 如何中断模型输出,使用工具?
- 现代工具调用 (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 执行操作时更可控,不会因为模型的误决策导致系统风险。