2023 年 6 月 OpenAI 研究与安全副总裁翁荔(Lilian Weng) 发表了一篇文章:《LLM-powered Autonomous Agents》可以翻译为 《由大语言模型驱动的自主智能体》这篇文章是 AI Agent 领域的奠基之作。它系统性地拆解了如何让大语言模型(LLM)从“只会说话的聊天机器人”进化为“能独立解决复杂问题的智能体”。
这篇文章提出的 Agent 基本架构如下所示:
然后今天在 Github 上看到一个教程,正好可以用来学习一下如何编写 Agent。不过这个教程是英文的,实在是不方便我复习,所以还是在这里自己记录一下学习的过程,以备复习吧。
https://github.com/shareAI-lab/learn-claude-code/blob/main/README-zh.md
像 2026年3月 大火的小龙虾🦞,还有 Claude Code 、Codex 、Trae 等等都是一个 Agent,在本质上跟本文写的 Agent 是一个东西,只是功能上复杂一点而已,学会了原理,你也可以写出一个很好用的 Agent。
S01 【The Agent Loop】 Agent 循环
最简单的 Agent 代码其实就是写一个 while 循环。当 AI 大语言模型需要现实世界的信息或操作现实世界的时候,让 Agent 去访问现实世界即可。
简单表达的话就是下面这种结构:
大语言模型可以推理代码,但无法触及现实世界——无法读取文件、运行测试或检查错误。如果没有循环,每次工具调用都需要你手动复制粘贴结果,你就成为了循环的一部分。所以我们写 Agent Harness 就是替换你在这个循环的中的作用。
由此可见,我们可以设计一个基本的逻辑,大语言模型可以多次调用工具,直到大语言模型认为任务完成了,才结束整个流程。
然后,我们来用 Python 代码实现一下上面的逻辑。不过在那之前,先去开通一下大模型的 API。开通那个平台倒是无所谓,用你最喜欢的就可以了,我直接注册了字节的火山引擎.... 不过还好,跟广告说的一样,送 50万 token,行吧先用着吧。
访问火山引擎,然后登陆,然后开通 API 获取 API key 即可。
然后调用一下 API 教程提供的测试代码,看看 API key 开通成功了没有。
先下载 openai SDK:
pip install --upgrade "openai>=1.0"
import os
from openai import OpenAI
from config import api_key
# 请确保您已将 API Key 存储在环境变量 ARK_API_KEY 中
# 初始化Ark客户端,从环境变量中读取您的API Key
client = OpenAI(
# 此为默认路径,您可根据业务所在地域进行配置
base_url="https://ark.cn-beijing.volces.com/api/v3",
# 为防止泄露,我把 api key 放在 config.py 中了,当然很多教程都是放在环境变量中的,都行吧。
api_key=api_key,
)
response = client.chat.completions.create(
# 指定您创建的方舟推理接入点 ID,此处已帮您修改为您的推理接入点 ID
model="doubao-seed-2-0-code-preview-260215",
messages=[
{
"role": "user",
"content": [
{
"type": "image_url",
"image_url": {
"url": "https://ark-project.tos-cn-beijing.ivolces.com/images/view.jpeg"
},
},
{"type": "text", "text": "这是哪里?你是谁?"},
],
}
],
)
print(response.choices[0])
这是模型返回的信息, 真能 BB ,浪费我 Token。
Choice(finish_reason='stop', index=0, logprobs=None, message=ChatCompletionMessage(content='这里可能是一处郊外的湖泊,周围有山脉和森林。我是豆包,是字节跳动开发的 AI,能帮你解答各种问题、聊天互动、提供信息等。', refusal=None, role='assistant', annotations=None, audio=None, function_call=None, tool_calls=None, reasoning_content='用户现在需要解决的问题是:首先看图片,是一个人在湖泊皮划艇,背景雪山森林。然后问题是“这是哪里?你是谁?”。首先,我作为AI,要说明我是豆包,是字节跳动的AI助手。然后关于“这是哪里”,因为图片是艺术创作或示意图,没有明确地标,所以说明无法确定具体地点,只能描述场景:一个有雪山、森林的湖泊,有人皮划艇。\n\n现在组织语言:\n\n首先,我是豆包,是字节跳动开发的AI助手,帮用户解答问题、聊天等。\n\n然后关于地点:这张图片展示的是一个自然风光场景,有宁静的湖泊、茂密的森林、远处的雪山,还有人在皮划艇,但因为没有具体地标(比如独特的建筑、地名标识),所以无法确定这是现实世界中的哪个具体地点,可能是艺术家创作的想象场景,或是类似北欧、阿拉斯加、加拿大这类有雪山湖泊森林的地区的概括性描绘。\n\n现在检查语言是否和问题一致(用户用中文,所以回答中文),结构清晰:先介绍自己,再分析地点。\n\n现在整理成自然的中文回答:\n\n我是豆包,是字节跳动打造的人工智能助手,能为你提供信息查询、聊天互动等帮助~ \n\n至于“这是哪里”,从图片来看,它呈现的是一处自然风光:平静的湖泊上有人划着皮划艇,周围是茂密的森林,远处矗立着雪山。但因为图片没有明确的地理标识(比如专属地名、独特建筑),所以无法确定这是现实中**具体哪个地点**——它可能是艺术家创作的想象场景,也可能是对北欧、阿拉斯加、加拿大落基山脉等“有雪山、森林、湖泊”的地区的概括性描绘哦~'))
然后让我们正式改造这段代码,让它变成一个 Agent 循环。
import json
import os
import subprocess
from volcenginesdkarkruntime import Ark
from config import api_key
首先引入一些包,config 包是我放 api_key 的 .py 文件。然后这个 volcenginesdkarkruntime 巨抽象的包名是字节火山引擎的包名,需要提前使用 pip install 'volcengine-python-sdk[ark]' 进行下载。
然后编写一些全局变量
# 为防止泄露,我把 api key 放在 config.py 中了,当然很多教程都是放在环境变量中的,都行吧。
client = Ark( api_key = api_key )
# 模型名称
MODEL = "doubao-seed-2-0-code-preview-260215"
# 系统顶级指令
SYSTEM = f"你是一个位于 {os.getcwd()} 工作目录的编程智能体。请使用 Bash 来解决任务。只管行动,不要解释。只管行动,不要解释。"
# 工具列表
TOOLS = [{
"type": "function",
"function": {
"name": "run_bash",
"description": "执行 shell 命令",
"parameters": {
"type": "object",
"properties": {
"command": {
"type": "string",
"description": "要执行的 shell 命令"
}
},
"required": ["command"]
}
},
}]
因为用的是火山引擎的 SDK ,直接连 baseurl 都不用写了,Ark 类自己知道自家的 baseurl 是啥。
这个工具列表是按照火山引擎的 Chat API 教程编写的,跟前面介绍的教程里的 Anthropic 家的 API 不太一样。
def run_bash(command: str) -> str:
dangerous = ["rm -rf /", "sudo", "shutdown", "reboot", "> /dev/"]
if any(d in command for d in dangerous):
return "Error: 危险命令,已被拦截"
# 在自己电脑上搞这种危险的操作,我还是挺怕怕的,还是加个人工操作吧。😂
tmp = input(f"系统检测到调用 run_bash 函数, 需要执行的命令为 >>> \033[31m{command}\033[0m <<< 是否允许 \n Y/N >>> ")
if tmp == "Y" or tmp == "y":
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 "(执行完毕,但无输出)"
except subprocess.TimeoutExpired:
return "Error: 命令执行失败,120 秒超时"
else:
return "Error: 命令执行失败,用户取消执行"
然后定义一个执行 bash 命令的函数。
# Agent 循环的核心方法
def agent_loop(messages: list):
while True:
response = client.chat.completions.create( model = MODEL, messages = messages, tools = TOOLS, max_tokens = 8000 )
# 将大模型返回的消息添加到消息列表中
message = response.choices[0].message
messages.append(message.model_dump())
# 如果大模型没有调用工具,那么结束执行
if response.choices[0].finish_reason != "tool_calls":
# 打印 AI 的响应
print(f"\n\033[36mAI: {message.content}\033[0m\n")
return
# 打印 AI 的响应
print(f"\n\033[36mAI: {message.reasoning_content}\033[0m\n")
if message.tool_calls:
for tool_call in message.tool_calls:
func_name = tool_call.function.name
func_args = json.loads(tool_call.function.arguments)
call_id = tool_call.id
# 3. 路由并执行本地函数
if func_name == "run_bash":
result = run_bash(command=func_args.get("command"))
print(f"\n\033[33mtools : {result}\033[0m\n")
else:
result = "Error: Function not found"
messages.append({"role": "tool", "content": result, "tool_call_id":call_id})
然后这里就是 Agent 核心循环了,当 AI 响应的 finish_reason 不是 tool_calls 的时候,直接结束循环,如果 AI 响应的 finish_reason 是 tool_calls 那么执行工具调用的逻辑。
这一句 messages.append(message.model_dump()) 中,message 是一个对象,不能直接加入到 list 中去,必须先转成 dict 才行。
这一句 message = response.choices[0].message 中的 choices[0] 是因为大模型在生成回复时,本质上是在做概率预测。所以,API 返回的 JSON 里会有一个 choices 列表,里面装着模型给出的所有“选项”,但是如果请求里没有特殊设置,这个 choices 列表就只有一个概率最大的选项而已。
顺便来看一下 AI 响应的数据响应体是什么样子:
{
"id": "02177485752710481a9193ce9f89e3456d252c7b9d01d0342be7e",
"choices": [
{
"finish_reason": "tool_calls",
"index": 0,
"logprobs": null,
"message": {
"content": "",
"role": "assistant",
"tool_calls": [
{
"id": "call_vi59nfph9j1ak81vexpl05hi",
"function": {
"arguments": "{\"command\": \"echo 'print(\\\"Hello, World!\\\")' > hello.py\"}",
"name": "run_bash"
},
"type": "function"
}
],
"reasoning_content": "用户让我创建一个叫hello.py的文件,打印\"Hello, World!\"。首先我需要确认当前目录,根据系统提示,我在/Users/sanqiushu/Code_Space/PyCharm_Space/Project/test_ai_agent/S01这个目录下。那我直接用echo命令或者cat来创建文件吧。比如用echo 'print(\"Hello, World!\")' > hello.py,这样应该可以。然后可能需要确认文件是否创建成功,不过用户没说,先创建再说。那我现在执行这个bash命令。"
}
}
],
"created": 1774857543,
"model": "doubao-seed-2-0-code-preview-260215",
"service_tier": "default",
"object": "chat.completion",
"usage": {
"completion_tokens": 167,
"prompt_tokens": 462,
"total_tokens": 629,
"prompt_tokens_details": {
"cached_tokens": 0
},
"completion_tokens_details": {
"reasoning_tokens": 122
}
}
}
然后是 main 函数:
# 主程序入口
if __name__ == "__main__":
# 注入模型需遵循的指令,包括扮演的角色、背景信息等。
history = [{"role": "system", "content": SYSTEM}]
try:
hi = "s01 :) 你好人类👋 (输入 exit 退出)"
query = input(f"\n\033[36m{hi}\033[0m\n>> ")
except (EOFError, KeyboardInterrupt):
exit()
if query.strip().lower() in ("q", "exit", ""):
exit()
history.append({"role": "user", "content": query})
agent_loop(history)
print(json.dumps(history, indent=4, ensure_ascii=False))
open("result.json", "w").write(json.dumps(history, indent=4, ensure_ascii=False))
然后展示一下这个 Agent 的运行过程吧:
执行完成。当然,你也可以在 main 函数里加 while 循环,以达到完成一个任务后,继续执行下一个对话,或者新建一个对话的功能。
总体来说,这个代码还是挺简单的,区区 100 行代码就完成了一个 Agent 最核心的功能,就是记录与 AI 大模型的历史对话、记录本地提供的工具目录,然后按照官方提供的数据格式,提交给 AI 大模型,AI 大模型就会懂得如何调用工具,说真的,有一种很神奇的感觉,像是在调用云端的脑力。