1. 从零构建AI编程代理的核心循环
作为一名长期从事AI工具开发的工程师,我发现很多开发者对构建AI编程代理存在误解,认为需要复杂的框架和大量工具。实际上,一个功能完备的AI代理核心只需要两个要素:一个循环结构和一个工具调用机制。这就是为什么我说"一个循环+Bash=一个智能体"。
1.1 为什么选择Bash作为基础工具
Bash shell在Unix-like系统中几乎无处不在,它提供了访问操作系统能力的完整接口。通过Bash,我们可以:
- 读写文件系统(cat, echo, >, >>等)
- 执行任意程序(python, git, make等)
- 管理进程(&, nohup, kill等)
- 处理文本数据(grep, awk, sed等)
python复制TOOLS = [{
"name": "bash",
"description": "Run a shell command.",
"input_schema": {
"type": "object",
"properties": {"command": {"type": "string"}},
"required": ["command"],
},
}]
这个简单的工具定义就足以让AI代理完成绝大多数编程任务。我曾在一个项目中尝试添加专门的read_file、write_file工具,结果发现这反而增加了模型的认知负担——模型需要学习多个工具的用法,而实际上它们都只是Bash命令的包装。
1.2 代理循环的工作原理
代理循环的核心逻辑异常简单:
python复制def agent_loop(messages: list):
while True:
# 1. 调用语言模型获取响应
response = client.messages.create(
model=MODEL, system=SYSTEM, messages=messages,
tools=TOOLS, max_tokens=8000,
)
# 2. 记录助手响应
messages.append({"role": "assistant", "content": response.content})
# 3. 检查是否应该结束循环
if response.stop_reason != "tool_use":
return
# 4. 执行所有工具调用
results = []
for block in response.content:
if block.type == "tool_use":
output = run_bash(block.input["command"])
results.append({
"type": "tool_result",
"tool_use_id": block.id,
"content": output
})
# 5. 将工具执行结果加入对话历史
messages.append({"role": "user", "content": results})
这个不到30行的函数就是整个智能体的核心。我曾用它来完成以下任务:
- 创建和修改Python文件
- 运行测试并分析结果
- 管理Git仓库
- 安装依赖项
- 甚至部署简单的Web服务
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 安全性与错误处理实践
2.1 危险命令拦截
允许AI直接执行Bash命令存在明显风险。在我的实现中,run_bash函数包含基本的防护:
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:
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)"
在实际项目中,我还会添加:
- 用户确认机制:对于高风险操作要求人工确认
- 沙箱环境:在容器中执行不确定的命令
- 命令白名单:只允许特定模式的命令
2.2 执行上下文管理
注意subprocess.run中的cwd=os.getcwd()参数。这确保了命令在预期的工作目录中执行。我曾遇到过因为忽略这一点导致的文件路径错误——AI生成的命令在错误的位置创建了文件,导致后续步骤失败。
3. 消息流分析与调试技巧
3.1 典型交互流程分析
让我们通过创建hello.py的完整流程,看看消息是如何演变的:
- 初始用户请求:
json复制[
{
"role": "user",
"content": "Create a file called hello.py that prints \"Hello, World!\""
}
]
- 模型首次响应(调用Bash):
json复制{
"content": [
{
"input": {"command": "echo 'print(\"Hello, World!\")' > hello.py"},
"name": "bash",
"type": "tool_use"
}
],
"stop_reason": "tool_use"
}
- 执行结果加入对话:
json复制{
"role": "user",
"content": [
{
"type": "tool_result",
"tool_use_id": "...",
"content": "(no output)"
}
]
}
- 模型验证结果:
json复制{
"content": [
{"text": "The file `hello.py` has been created..."},
{
"input": {"command": "cat hello.py && python hello.py"},
"name": "bash",
"type": "tool_use"
}
],
"stop_reason": "tool_use"
}
- 最终确认:
json复制{
"content": [
{"text": "The file `hello.py` has been successfully created..."}
],
"stop_reason": "end_turn"
}
3.2 调试日志的重要性
代码中的print语句不是随意的——它们构成了调试基础设施:
python复制print("assistant response:", response.to_json())
print(f"\033[33m$ {block.input['command']}\033[0m") # 黄色显示执行的命令
print(output[:200]) # 截断长输出
print("messages:", json.dumps(messages, indent=2, ensure_ascii=False))
这些日志帮助我快速定位了以下类型的问题:
- 模型误解任务要求
- 命令执行失败但返回了成功状态码
- 工具结果格式不符合模型预期
- 对话历史积累导致的上下文窗口溢出
4. 系统提示词设计艺术
SYSTEM变量的内容对代理行为有决定性影响:
python复制SYSTEM = f"You are a coding agent at {os.getcwd()}. Use bash to solve tasks. Act, don't explain."
这个简洁的提示词包含几个关键要素:
- 角色定位:明确告知模型它是一个"coding agent"
- 工作目录:提供上下文信息,帮助模型生成正确的路径
- 行动导向:"Act, don't explain"抑制了模型过度解释的倾向
在我的实验中,更详细的提示词反而会降低效率。例如,添加"生成完整代码"这样的要求会导致模型在应该执行命令时却开始写长篇代码解释。
5. 性能优化与扩展思路
5.1 控制上下文长度
随着对话轮次增加,messages列表会不断增长。实践中我采用以下策略:
- 自动摘要:定期用模型总结对话历史
- 关键信息提取:只保留必要的工具调用结果
- 分页处理:将长输出拆分为多个部分
5.2 扩展为多工具代理
虽然单一Bash工具已经很强大,但特定场景下添加专用工具仍有价值:
python复制TOOLS = [
{
"name": "bash",
"description": "Run shell commands",
# ...原有定义...
},
{
"name": "python_repl",
"description": "Execute Python code interactively",
"input_schema": {
"type": "object",
"properties": {"code": {"type": "string"}},
"required": ["code"],
}
}
]
引入新工具时要注意:
- 确保功能不重复(如python_repl应提供Bash无法实现的交互式执行)
- 保持接口简单
- 提供清晰的描述和示例
6. 完整代码实现解析
让我们拆解核心代码的每个部分:
python复制#!/usr/bin/env python3
"""
s01_agent_loop.py - The Agent Loop
...
"""
import json
import os
import subprocess
from anthropic import Anthropic
from dotenv import load_dotenv
# 环境变量加载
load_dotenv(override=True)
if os.getenv("ANTHROPIC_BASE_URL"):
os.environ.pop("ANTHROPIC_AUTH_TOKEN", None)
# 客户端初始化
client = Anthropic(base_url=os.getenv("ANTHROPIC_BASE_URL"))
MODEL = os.environ["MODEL_ID"]
# Bash工具实现
def run_bash(command: str) -> str:
# ...如前所述...
# 主循环实现
def agent_loop(messages: list):
# ...如前所述...
# 交互式界面
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_loop(history)
# 显示最后响应
response_content = history[-1]["content"]
if isinstance(response_content, list):
for block in response_content:
if hasattr(block, "text"):
print(block.text)
print()
这个实现的特点包括:
- 环境隔离:通过dotenv管理敏感信息
- 交互式界面:方便测试和演示
- 彩色输出:提升可读性
- 健壮的错误处理:防止意外中断
7. 实际应用案例与技巧
7.1 自动化测试辅助
我经常用这个代理来:
- 根据测试失败信息修改代码
- 重新运行特定测试用例
- 分析测试覆盖率
例如:
code复制s01 >> 运行pytest tests/test_models.py::TestUser::test_create并显示最后5行输出
代理会自动执行命令并返回关键信息。
7.2 项目初始化模板
通过组合多个Bash命令,可以快速创建项目结构:
code复制s01 >> 创建Python项目结构:src/package/__init__.py, tests/, pyproject.toml, README.md
7.3 调试技巧
- 使用
set -x:在Bash命令前加上这个参数可以看到详细执行过程 - 分步验证:对于复杂任务,让代理逐步确认每个步骤的结果
- 人工检查点:在关键操作前插入确认提示
8. 性能考量与最佳实践
-
延迟优化:
- 并行执行独立命令
- 缓存常用命令结果
- 预加载模型
-
资源管理:
- 限制单次命令执行时间
- 监控内存使用
- 实现命令队列
-
用户体验:
- 提供进度反馈
- 支持任务中断
- 实现结果预览
在我的基准测试中,这个简单代理可以完成约70%的日常编程辅助任务,而响应时间通常在2-5秒之间。对于更复杂的场景,我会在此基础上构建分层架构,但核心始终是这个简洁的循环机制。
