导入
Agent
是什么?这段时间deepseek R1
爆火,当大家使用deepseek R1
进行聊天的时候,我们是一问一答的形式,和之前的chat GPT
一样。这样的对话形式的AI有一个缺点,那就是不能和电脑本地环境进行交互。比如你让它们生成一段代码,它们直接给你代码,无法帮你在本地建立文件,然后写入,甚至运行测试,你只能拷贝它们给出的代码然后自己建立文件然后粘贴进去,并且编译或者解释运行。而Agent
就是能够做到这一点,你可能想到了cursor
,同样大火的一款IDE
,没错,Cursor IDE
就是一个Agent
。
而实现一个Agent
其实跟大模型本身没有关系,我们不需要修改任何的大模型部分,重点是prompt 提示词
。可能不好理解,但是其实很简单,我举个例子就都懂了。比如:还是上面提到的生成代码任务,我现在想让大模型帮我生成一个小游戏代码,并且直接本地新建文件写入。我可以写类似这样的提示词,请在本地当前目录下新建一个文件,然后帮我生成一个html射击小游戏,直接写入这个文件。你可以这样调用本地工具<tool>write_file(path,content)</tool>,作用是把content内容写入path目录文件中,请等待工具执行结果(由我来确认),在获得工具执行结果前停止执行,获得结果后根据执行结果选择完成或者重新尝试
。这样大模型就会以为本地有一个可用的写入文件的工具,并且按照<tool>write_file(path,content)</tool>
格式进行调用,如果我回复调用成功,大模型就会认为文件写入成功,并且完成任务。而实际上大模型本身并没有调用工具和写入本地文件的能力,上面这行格式只是一行文字,一行字符串而已。那么谁来调用呢?开发者来调用,开发者编写Agent主程序
解析大模型的返回,如果解析到<tool>write_file(path,content)</tool>
这行,就调用本地的工具函数写入,然后返回写入成功,再次传入大模型,大模型看到执行成功,就认为完成了工具调用,然后可以继续完成下面的流程。
因此可以得到Agent
的关键三要素大模型,Agent主程序,工具。
流程
从上面可以看到,要想实现Agent
,我们必须严格大模型的输出格式和终止流程时机。我们可以在系统提示词system prompt
中严格要求大模型,然后用户提示词user prompt
中,输入用户任务要求,任务完成前每次大模型的输出都是辅助提示词asistant prompt
。
而Agent的完整流程是:大模型进行思考thought
,然后选择调用哪个工具tool
,根据返回结果继续思考thought
,如果返回成功就输出完成,否则继续调用工具tool
,如此往复循环。
一个系统提示词system prompt
的示例如下:
请按照如下流程解决任务:
首先进行思考,使用<thought>标签包围思考内容。然后决定使用哪个可用工具,用<tool>标签包围工具。接着,将根据工具的返回结果得到<observation>标签包围的观察结果。再根据观察结果继续思考,如此循环,直到可以给出<answer>标签包围的最终答案。
例子:
<question>中国首都是哪里?</question>
<thought>我需要找到中国的首都,可以使用搜索工具。</thought>
<tool>get_capital("中国")</tool>
<observation>中国首都是北京。</observation>
<thought>搜索结果显示中国首都是北京,得到答案。</thought>
<answer>中国首都是北京。</answer>
请严格遵守:
- 输出格式严格按照XML格式输出
- 你每次回答都必须包括<thought>标签 以及 <tool>标签 或 <answer>标签
- 输出 <tool> 后立即停止生成,等待工具返回的 <observation>,自己不要生成<observation>
- 工具参数中的文件路径请使用绝对路径
可用工具列表:
- read_file(file_path): 用于读取文件内容
- write_file(file_path, content): 将指定内容写入指定文件
- run_command(command): 用于执行终端命令
环境信息:
操作系统:Windows
从上面提示词可以总结出代码的执行流程:
-
上面的系统提示词 + user用户提示词(+工具返回观察
<observation>
,如果不是第一次的话)传入大模型(使用openai库和openrouter API即可) -
获取大模型返回,一定包含
<thought>
标签和<tool>
标签或者<answer>
标签,此时表示任务完成 -
检测返回是否包含
<answer>
标签,是则直接返回结果,不是则继续检测<tool>
标签 -
解析
<tool>
标签包围的内容,获取使用到的工具函数名和参数,并且本地调用函数,如果是终端命令工具,则设置需要用户输入y同意才能进行,因为比较危险,工具执行结果用<observation>
标签包围后一起传入到大模型中,继续这个循环即可
实现Agent
主循环按照上面的流程即可,这也是最核心的代码:
为了读者更直观,我再强调下,大模型返回的数据形式大概是什么样的:
<thought>用户要求在当前目录下新建一个文件夹,并在其中创建一个HTML射击小游戏。
我需要先创建文件夹,然后编写HTML文件。首先使用run_command命令创建文件夹。</thought>
<tool>run_command(mkdir game_folder)</tool>
<thought>
文件夹创建成功。现在需要在game_folder目录下创建一个HTML射击游戏。
我需要编写一个包含基本射 击游戏功能的HTML文件。我将创建一个简单的canvas射击游戏。
</thought>
<tool>write_file("physics_demo/ball_physics.py", "
<!DOCTYPE html>
<html>
<head>
<title>射击小游戏</title>
....省略
</body>
</html>
“)</tool>
<thought>
HTML射击游戏文件已经成功创建在game_folder目录中。
。。。。。。省略
</thought>
对着上面的形式,下面的正则表达式应该就十分清晰了。
def run(self, user_input: str):
messages = [
{"role": "system", "content": system_prompt},
{"role": "user", "content": f"<question>{user_input}</question>"}
]
while True:
content = self.call_model(messages)
thought_match = re.search(r"<thought>(.*?)</thought>", content, re.DOTALL)
if thought_match:
thought = thought_match.group(1)
print(f"\n Thought: {thought}")
# 检测模型是否输出最终答案
if "<answer>" in content:
final_answer = re.search(r"<answer>(.*?)</answer>", content, re.DOTALL)
return final_answer.group(1)
# 检测工具使用
tool_found = re.search(r"<tool>(.*?)</tool>", content, re.DOTALL)
if not tool_found:
raise RuntimeError("模型未输出 <tool>")
tool = tool_found.group(1)
tool_name, args = self.parse_toolfunc(tool)
print(f"\n Tool: {tool_name}({', '.join(args)})")
# 终端命令需要询问用户
should_continue = input(f"\n\n是否继续?(Y/N)") if tool_name == "run_command" else "y"
if should_continue.lower() != 'y':
print("\n 操作已取消。")
return "操作被用户取消"
try:
observation = self.tools[tool_name](*args)
except Exception as e:
observation = f"工具执行错误:{str(e)}"
print(f"\n Observation:{observation}")
obs_msg = f"<observation>{observation}</observation>"
messages.append({"role": "user", "content": obs_msg})
调用大模型,使用openai库即可,deepseek/deepseek-chat-v3.1:free
这个免费模型,当然使用收费模型效果更好,在openrouter中搜索这个模型,就可以看到API调用格式:
from openai import OpenAI
client = OpenAI(
base_url="https://openrouter.ai/api/v1",
api_key="<OPENROUTER_API_KEY>",
)
completion = client.chat.completions.create(
extra_headers={
"HTTP-Referer": "<YOUR_SITE_URL>", # Optional. Site URL for rankings on openrouter.ai.
"X-Title": "<YOUR_SITE_NAME>", # Optional. Site title for rankings on openrouter.ai.
},
extra_body={},
model="deepseek/deepseek-chat-v3.1:free",
messages=[
{
"role": "user",
"content": "What is the meaning of life?"
}
]
)
print(completion.choices[0].message.content)
就参考这个形式即可。解析工具函数的名字和参数,可以使用ast模块,也很简单:
def parse_toolfunc(self, code_str):
"""工具函数名和参数解析"""
try:
# 使用ast模块解析函数调用
tree = ast.parse(code_str.strip(), mode='eval')
if not isinstance(tree.body, ast.Call):
raise ValueError("不是有效的函数调用")
func_name = tree.body.func.id
args = []
for arg in tree.body.args:
# 对每个参数节点求值
try:
arg_value = ast.literal_eval(arg)
except (ValueError, SyntaxError):
# 处理无法直接求值的复杂表达式
arg_value = ast.get_source_segment(code_str, arg)
args.append(arg_value)
return func_name, args
except SyntaxError:
raise ValueError("Invalid function call syntax")
工具函数则是:
def read_file(file_path):
"""用于读取文件内容"""
with open(file_path, "r", encoding="utf-8") as f:
return f.read()
def write_file(file_path, content):
"""将指定内容写入指定文件"""
with open(file_path, "w", encoding="utf-8") as f:
f.write(content.replace("\\n", "\n"))
return "写入成功"
def run_command(command):
"""用于执行终端命令"""
import subprocess
run_result = subprocess.run(command, shell=True, capture_output=True, text=True)
return "执行成功" if run_result.returncode == 0 else run_result.stderr
完整代码
系统提示词文件system_prompt.py
:
system_prompt = """
请按照如下流程解决任务:
首先进行思考,使用<thought>标签包围思考内容。然后决定使用哪个可用工具,用<tool>标签包围工具。接着,将根据工具的返回结果得到<observation>标签包围的观察结果。再根据观察结果继续思考,如此循环,知道可以给出<answer>标签包围的最终答案。
例子:
<question>中国首都是哪里?</question>
<thought>我需要找到中国的首都,可以使用搜索工具。</thought>
<tool>get_capital("中国")</tool>
<observation>中国首都是北京。</observation>
<thought>搜索结果显示中国首都是北京,得到答案。</thought>
<answer>中国首都是北京。</answer>
请严格遵守:
- 输出格式严格按照XML格式输出
- 你每次回答都必须包括<thought>标签 以及 <tool>标签 或 <answer>标签
- 输出 <tool> 后立即停止生成,等待工具返回的 <observation>,自己不要生成<observation>
- 工具参数中的文件路径请使用绝对路径
可用工具列表:
- read_file(file_path): 用于读取文件内容
- write_file(file_path, content): 将指定内容写入指定文件
- run_command(command): 用于执行终端命令
环境信息:
操作系统:Windows
"""
工具类文件tools.py
:
def read_file(file_path):
"""用于读取文件内容"""
with open(file_path, "r", encoding="utf-8") as f:
return f.read()
def write_file(file_path, content):
"""将指定内容写入指定文件"""
with open(file_path, "w", encoding="utf-8") as f:
f.write(content.replace("\\n", "\n"))
return "写入成功"
def run_command(command):
"""用于执行终端命令"""
import subprocess
run_result = subprocess.run(command, shell=True, capture_output=True, text=True)
return "执行成功" if run_result.returncode == 0 else run_result.stderr
tools_list = [read_file, write_file, run_command]
Agent主文件agent.py
:(替换成你自己的API key即可,因为API提供商非常之多,如果不用openrouter的话,而使用比如硅基流动之类的话,替换对应的base_url
和你要使用的model
即可,这里就以免费的deepseek-chat-v3.1 API为例)
import ast
import os
import re
from openai import OpenAI
from system_prompt import system_prompt
import tools
class Agent:
def __init__(self, tools):
self.tools = { func.__name__: func for func in tools }
self.model = "deepseek/deepseek-chat-v3.1:free"
self.client = OpenAI(
base_url="https://openrouter.ai/api/v1",
api_key="替换成你的APIkey",
)
def run(self, user_input: str):
messages = [
{"role": "system", "content": system_prompt},
{"role": "user", "content": f"<question>{user_input}</question>"}
]
while True:
content = self.call_model(messages)
thought_match = re.search(r"<thought>(.*?)</thought>", content, re.DOTALL)
if thought_match:
thought = thought_match.group(1)
print(f"\n Thought: {thought}")
# 检测模型是否输出最终答案
if "<answer>" in content:
final_answer = re.search(r"<answer>(.*?)</answer>", content, re.DOTALL)
return final_answer.group(1)
# 检测工具使用
tool_found = re.search(r"<tool>(.*?)</tool>", content, re.DOTALL)
if not tool_found:
raise RuntimeError("模型未输出 <tool>")
tool = tool_found.group(1)
tool_name, args = self.parse_toolfunc(tool)
print(f"\n Tool: {tool_name}({', '.join(args)})")
# 终端命令需要询问用户
should_continue = input(f"\n\n是否继续?(Y/N)") if tool_name == "run_command" else "y"
if should_continue.lower() != 'y':
print("\n 操作已取消。")
return "操作被用户取消"
try:
observation = self.tools[tool_name](*args)
except Exception as e:
observation = f"工具执行错误:{str(e)}"
print(f"\n Observation:{observation}")
obs_msg = f"<observation>{observation}</observation>"
messages.append({"role": "user", "content": obs_msg})
def call_model(self, messages):
print("\n 正在调用大模型,请稍等...")
response = self.client.chat.completions.create(
model=self.model,
messages=messages,
)
content = response.choices[0].message.content
messages.append({"role": "assistant", "content": content})
return content
def parse_toolfunc(self, code_str):
"""工具函数名和参数解析"""
try:
# 使用ast模块解析函数调用
tree = ast.parse(code_str.strip(), mode='eval')
if not isinstance(tree.body, ast.Call):
raise ValueError("不是有效的函数调用")
func_name = tree.body.func.id
args = []
for arg in tree.body.args:
# 对每个参数节点求值
try:
arg_value = ast.literal_eval(arg)
except (ValueError, SyntaxError):
# 处理无法直接求值的复杂表达式
arg_value = ast.get_source_segment(code_str, arg)
args.append(arg_value)
return func_name, args
except SyntaxError:
raise ValueError("Invalid function call syntax")
def main():
agent = Agent(tools=tools.tools_list)
task = input("输入任务描述:")
final_answer = agent.run(task)
print(f"\n 最终答案:{final_answer}")
if __name__ == "__main__":
main()
结果
运行python agent.py
即可,用一个简单的用户任务为例:帮我在当前目录下新建一个文件夹,然后在新建文件夹内写一个html的射击小游戏。
输出如下:
输入任务描述:帮我在当前目录下新建一个文件夹,然后在新建文件夹内写一个html的射击小游戏
正在调用大模型,请稍等...
Thought: 用户要求在当前目录下新建一个文件夹,并在其中创建一个HTML射击小游戏。我需要先创建文件夹,然后编写HTML文件。首先使用run_command命令创建文件夹。
Tool: run_command(mkdir game_folder)
是否继续?(Y/N)y
Observation:执行成功
正在调用大模型,请稍等...
Thought: 文件夹创建成功。现在需要在game_folder目录下创建一个HTML射击游戏。我需要编写一个包含基本射 击游戏功能的HTML文件。我将创建一个简单的canvas射击游戏。
Tool: write_file(game_folder/shooting_game.html, <!DOCTYPE html>
<html>
<head>
<title>射击小游戏</title>
... //中间省略
// 游戏循环
function gameLoop() {
update();
draw();
requestAnimationFrame(gameLoop);
}
// 启动游戏
gameLoop();
</script>
</body>
</html>)
Observation:写入成功
正在调用大模型,请稍等...
Thought: HTML射击游戏文件已经成功创建在game_folder目录中。游戏包含基本的射击功能:玩家可以使用左右 箭头键移动,空格键射击,敌人会从上方生成,击中敌人可以得分,碰到敌人则游戏结束。现在可以给用户确认完成。
最终答案:已完成!在当前目录下创建了game_folder文件夹,并在其中创建了一个HTML射击小游戏。游戏功能包括:
- 使用左右箭头键移动玩家
- 空格键射击
- 敌人从上方生成
- 击中敌人得分
- 碰到敌人游戏结束
您可以通过浏览器打开 game_folder/shooting_game.html 文件来玩游戏。
可以看到大模型首先思考,并且返回工具调用请求,即run_command(mkdir game_folder)
,也就是生成一个新的文件夹,并且征求我们的意见,因为是命令行工具,同意后,生成html代码,然后写入文件,然后给出最终答案。(其中代码部分太长,中间省略)
游戏结果:
可以看到游戏运行良好。
这个例子比较简单,Agent只调用了2次工具,让我们再用一个调用一点的例子看一看,比如:在当前目录下新建一个文件夹,在新建文件夹内帮我实现一个项目,用python编写一个小球落地的物理效果演示
Thought: 用户要求在当前目录下创建一个新文件夹,并在其中用Python实现一个小球落地的物理效果演示。我需要先创建文件夹,然后编写Python代码来实现物理模拟。首先,我应该使用run_command来创建文件夹。
Tool: run_command(mkdir physics_demo)
是否继续?(Y/N)y
Observation:执行成功
正在调用大模型,请稍等...
Thought: 文件夹已创建成功。现在需要在physics_demo文件夹中创建一个Python文件来实现小球落地的物理效果。我将使用write_file来创建并写入Python代码。代码应该模拟小球受重力影响下落并在触地时反弹的物理效果,可以使用pygame库来实现图形化演示。
Tool: write_file(physics_demo/ball_physics.py, import pygame
import sys
# 初始化pygame
pygame.init()
...#省略
pygame.quit()
sys.exit()
Thought: 文件内容读取成功,代码看起来正确。现在让我创建一个README文件来说明如何运行这个项目,并测试一下代码是否能正常运行。
Tool: write_file(physics_demo/README.md, # 小球落地物理演示
## 项目描述
这是一个使用Python和Pygame库实现的小球落地物理效果演示程序
。 。。。。。省略
Observation:写入成功
正在调用大模型,请稍等...
Thought: 让我使用更详细的命令来查看physics_demo文件夹中的具体文件,确认ball_physics.py文件确实存在。
Tool: run_command(dir physics_demo/a)
Observation:执行成功
正在调用大模型,请稍等...
Thought: 让我使用更具体的命令来查看physics_demo文件夹的内容,确认文件确实创建成功。
Tool: run_command(dir physics_demo\*.*)
Observation:执行成功
Thought: README文件已创建成功。现在让我测试一下程序是否能正常运行。首先需要检查当前目录是否安装了pygame,如果没有的话需要安装。让我先尝试运行Python程序。
Tool: run_command(cd physics_demo && python ball_physics.py)
是否继续?(Y/N)y
.....省略
可以看到不仅生成了代码文件,还生成了README.md
说明文件,并且还尝试不同的方法确认项目文件夹和文件是否存在,并且自动运行了测试。
运行效果如下,可以看到运行也很不错:
总结
主要讲解了Agent
这个技术的底层原理,实则就是大模型通过Agent主流程调用工具,但是就可以有比单纯的Chat更强的交互感,算是提示词工程的一次进化,由prompt engineering 提示词工程
进化成agent engineering 智能体工程
。且随着可用工具的增加,Agent
可以变得非常强大,比如网络搜索工具,可以联机搜索更多的信息;文生图工具,可以根据提示词本地生成图片;不同格式文件分析器工具,可以分析不同文件的内容等。以及MCP
协议,专门标准化大模型和工具之间的接口,虽然MCP本质上还是一个程序,比如python程序或者nodejs程序,上面演示的tool.py
模块其实就是MCP的雏形,只不过是直接import模块调用,而MCP是是通过各种协议接口调用,比如stdio,SSE,streamableHttp
,但是因为标准化了接口和数据交换格式,使得各种软件都可以实现MCP作为工具,比如支持MCP的地图软件,可以作为可用工具被大模型调用,使得大模型可以为你规划路线等。并且工具类型和大模型类型的倾向不同,不同Agent
的适用领域也不同,金融领域,编程领域,日常生活领域等等不同的Agent
可以更具针对性。
之前说,AI大模型使用工具,其实就是提示词,其实不准确,应该说提示词是实现调用工具的一种最直接,也最巧妙的方式,就像上面的代码例子一样,也可以说是相对早期的一个方式,因为随着工具的增多,单纯在系统提示词中添加工具函数列表效率肯定是低下的,因此肯定会被替代。AI Agent
调用工具还可以是Function Calling
的方式,这是OpenAI
提出的一种方式,大模型会自动调用工具,不需要添加到提示词中,只需要在API调用中设置好即可。可以认为Function Calling, MCP
是更加现代的Agent
实现工具调用的方式,而system prompt
提示词是早期方式。