建議收藏:想做AI編程產(chǎn)品?先從這段不到400行的Agent代碼開始! 精華
前言
去年以來(lái),以Cursor為代表的AI編程工具橫空出世,徹底點(diǎn)燃了全球開發(fā)者對(duì)AI輔助編程的熱情。海外各種新穎的AI開發(fā)工具層出不窮,幾乎每周都有新的概念或產(chǎn)品涌現(xiàn)。反觀國(guó)內(nèi),除了幾家互聯(lián)網(wǎng)大廠有所布局,專注于AI編程工具的初創(chuàng)公司似乎相對(duì)較少。這固然有國(guó)內(nèi)大模型編程能力仍在追趕的原因,但或許也有一部分原因是,很多人覺得構(gòu)建一個(gè)AI編程工具,特別是具備復(fù)雜交互和能力的“智能體(Agent)”,門檻很高,非常復(fù)雜。
事實(shí)真的如此嗎?今天,我們就嘗試用不到400行Python代碼,帶你從零實(shí)現(xiàn)一個(gè)簡(jiǎn)單的AI編程智能體。通過(guò)這個(gè)例子,我們將揭示AI編程智能體的核心原理,希望能打消一些顧慮,為大家構(gòu)建自己的AI編程產(chǎn)品提供一些啟發(fā)和參考!
1. AI編程智能體的基本框架
一個(gè)AI智能體并非無(wú)所不能的神祇,它的核心是大模型 (LLM),但大模型本身是沒有感知外部環(huán)境和執(zhí)行外部動(dòng)作能力的。要讓大模型變得“智能”起來(lái),能夠完成實(shí)際任務(wù)(比如讀取文件、修改代碼),就需要賦予它工具 (Tools),并構(gòu)建一個(gè)“感知-決策-行動(dòng)”的循環(huán)來(lái)協(xié)調(diào)這一切。
用一個(gè)簡(jiǎn)單的框架來(lái)描述:
- 感知 (Perception):智能體接收用戶的指令或環(huán)境信息(例如用戶說(shuō)“幫我讀一下某個(gè)文件”)。
- 決策 (Reasoning):大模型根據(jù)指令和它掌握的工具信息進(jìn)行思考和規(guī)劃,決定下一步做什么。它可能會(huì)決定需要調(diào)用某個(gè)工具來(lái)獲取更多信息,或者直接給出答案,或者決定調(diào)用某個(gè)工具來(lái)執(zhí)行一個(gè)動(dòng)作。
- 行動(dòng) (Action):如果大模型決定調(diào)用工具,它會(huì)輸出一個(gè)特定的格式來(lái)表明它想調(diào)用哪個(gè)工具以及傳入什么參數(shù)。
- 執(zhí)行 (Execution):開發(fā)者編寫的“調(diào)度層”代碼會(huì)捕獲大模型的工具調(diào)用指令,并真正執(zhí)行對(duì)應(yīng)的工具函數(shù)。
- 觀察 (Observation):工具執(zhí)行完成后,會(huì)產(chǎn)生一個(gè)結(jié)果(例如文件內(nèi)容、執(zhí)行成功/失敗信息)。
- 反饋 (Feedback):工具的執(zhí)行結(jié)果被反饋給大模型,作為新的輸入信息。
- 再?zèng)Q策/輸出 (Re-Reasoning/Output):大模型接收到工具結(jié)果后,會(huì)再次決策:是根據(jù)結(jié)果繼續(xù)調(diào)用其他工具,還是已經(jīng)獲得足夠信息,可以直接生成最終回復(fù)給用戶。
這個(gè)循環(huán)不斷往復(fù),直到任務(wù)完成。我們的不到400行代碼,就是要實(shí)現(xiàn)這個(gè)循環(huán)中的關(guān)鍵環(huán)節(jié):工具的定義、大模型調(diào)用工具的解析、工具的執(zhí)行以及結(jié)果的反饋。
下面通過(guò)一個(gè)流程圖來(lái)更直觀地展示這個(gè)基本框架:
“感知-決策-行動(dòng)”循環(huán)
2. 智能體如何調(diào)用工具?核心機(jī)制解析
這是AI Agent的核心魔法之一:大模型如何知道要調(diào)用工具,以及如何告訴外部系統(tǒng)它想調(diào)用哪個(gè)工具和參數(shù)?
在深入代碼之前,我們可以通過(guò)一個(gè)簡(jiǎn)單的例子來(lái)建立直觀的理解。假設(shè)我們告訴大模型,它有一個(gè)叫做 ??get_weather(<地點(diǎn)名稱>)?
? 的工具,調(diào)用它可以獲取指定地點(diǎn)的天氣信息。
對(duì)話示例:
- 你:你是一個(gè)天氣專家。當(dāng)我問(wèn)你某個(gè)地方的天氣時(shí),你只需要回復(fù)我?
?get_weather(<地點(diǎn)名稱>)?
?。然后我會(huì)告訴你那里的實(shí)際天氣。明白了嗎? - 大模型:明白了。當(dāng)你問(wèn)我某個(gè)地方的天氣時(shí),我會(huì)回復(fù)?
?get_weather(<地點(diǎn)名稱>)?
?,然后你會(huì)提供具體的天氣信息。 - 你:好的,那慕尼黑天氣怎么樣?
- 大模型:?
?get_weather(慕尼黑)?
? - 你:慕尼黑現(xiàn)在天氣:炎熱潮濕,28攝氏度。
- 大模型:好的,我已經(jīng)知道慕尼黑的天氣是炎熱潮濕,28攝氏度了。下一個(gè)地方呢?比如巴黎天氣如何?
這個(gè)例子非常清晰地展示了工具調(diào)用的核心思想:我們告訴大模型它有哪些工具(盡管這里沒有提供正式的Schema,只是通過(guò)指令),當(dāng)用戶需求出現(xiàn)時(shí),大模型“決定”要使用某個(gè)工具,并以約定的格式(這里是 ??get_weather(...)?
? 字符串)告訴我們它想調(diào)用的工具和參數(shù)。然后,外部系統(tǒng)(也就是我們)負(fù)責(zé)“執(zhí)行”這個(gè)工具(這里是我們手動(dòng)提供了天氣信息),并將結(jié)果“反饋”給大模型,大模型再利用這個(gè)信息生成最終的用戶回復(fù)。
理解了這個(gè)“大模型輸出指令 -> 外部代碼執(zhí)行 -> 結(jié)果反饋回大模型”的循環(huán),你就抓住了Agent工具調(diào)用的核心。
現(xiàn)在,我們來(lái)看看在實(shí)際編程中如何實(shí)現(xiàn)這一機(jī)制。訣竅在于兩個(gè)關(guān)鍵點(diǎn):
- 工具定義 (Tool Definition / Schema):我們?cè)谡{(diào)用大模型API時(shí),會(huì)額外提供一個(gè)參數(shù),告訴模型它“擁有”哪些工具,每個(gè)工具叫什么名字,是用來(lái)做什么的,以及調(diào)用它需要哪些參數(shù)(參數(shù)名、類型、描述)。這通常是通過(guò)一個(gè)結(jié)構(gòu)化的數(shù)據(jù)格式來(lái)描述,比如JSON Schema。這些信息相當(dāng)于給了大模型一本“工具書”。
- 結(jié)構(gòu)化輸出 (Structured Output):當(dāng)大模型在決策階段認(rèn)為調(diào)用某個(gè)工具能更好地完成任務(wù)時(shí),它不會(huì)直接返回自然語(yǔ)言回復(fù),而是會(huì)按照API約定的格式,輸出一個(gè)結(jié)構(gòu)化的信息,明確指示:“我決定調(diào)用工具A,參數(shù)是X和Y”。
讓我們看看具體如何操作。假設(shè)我們有一個(gè)??read_file?
?函數(shù),用來(lái)讀取文件內(nèi)容。我們需要定義它的Schema:
# 這是一個(gè)示例的JSON Schema定義
read_file_schema = {
"type": "function",
"function": {
"name": "read_file", # 工具名稱
"description": "讀取指定路徑文件的內(nèi)容", # 工具描述
"parameters": { # 參數(shù)定義
"type": "object",
"properties": {
"path": { # 參數(shù)名
"type": "string", # 參數(shù)類型
"description": "要讀取文件的相對(duì)路徑"# 參數(shù)描述
}
},
"required": ["path"] # 必需的參數(shù)
}
}
}
在調(diào)用支持工具調(diào)用的LLM API時(shí)(例如OpenAI, Together AI, 或國(guó)內(nèi)一些大模型的Function Calling接口),我們會(huì)把這個(gè)Schema列表作為參數(shù)傳進(jìn)去。
當(dāng)用戶輸入“幫我讀取 ??/path/to/your/file.txt?
?? 這個(gè)文件的內(nèi)容”時(shí),如果大模型認(rèn)為??read_file?
?工具可以完成這個(gè)任務(wù),它就可能返回類似這樣的結(jié)構(gòu)化輸出:
{
"tool_calls": [
{
"id": "call_abc123", # 調(diào)用ID
"type": "function",
"function": {
"name": "read_file", # 模型決定調(diào)用的工具名稱
"arguments": "{\"path\": \"/path/to/your/file.txt\"}" # 模型決定的參數(shù),通常是JSON字符串
}
}
],
"role": "assistant",
"content": null # 如果模型只調(diào)用工具,content可能為空
}
關(guān)鍵點(diǎn)來(lái)了: 大模型只是告訴你它“想”干什么,具體的執(zhí)行必須由我們編寫的外部代碼來(lái)完成。我們的代碼需要:
- 檢查大模型的回復(fù)中是否包含?
?tool_calls?
?。 - 如果包含,解析出工具的名稱 (?
?function.name?
??) 和參數(shù) (??function.arguments?
?)。 - 根據(jù)工具名稱,調(diào)用我們實(shí)際定義的Python函數(shù)(比如查找一個(gè)函數(shù)映射表)。
- 執(zhí)行對(duì)應(yīng)的函數(shù),并將解析出的參數(shù)傳進(jìn)去。
- 將函數(shù)執(zhí)行的結(jié)果,按照API的要求格式化,添加回對(duì)話歷史中,并再次調(diào)用大模型。這次調(diào)用時(shí),大模型就能看到“工具調(diào)用的結(jié)果是XXX”,然后才能根據(jù)這個(gè)結(jié)果生成最終的用戶回復(fù)。
理解了這個(gè)“大模型輸出指令 -> 外部代碼執(zhí)行 -> 結(jié)果反饋回大模型”的循環(huán),你就抓住了Agent工具調(diào)用的核心。
3. 構(gòu)建我們的AI編程智能體
現(xiàn)在,我們來(lái)實(shí)現(xiàn)一個(gè)簡(jiǎn)單的AI編程智能體,它擁有讀文件、列文件和編輯文件三個(gè)基礎(chǔ)的編程工具。我們將代碼整合在一起,看看它有多簡(jiǎn)單。
首先,安裝并導(dǎo)入必要的庫(kù)(這里我們使用一個(gè)通用的??client?
?對(duì)象代表任何支持工具調(diào)用的LLM客戶端,讀者可以根據(jù)實(shí)際情況替換為OpenAI, Together AI或其他國(guó)內(nèi)廠商的SDK):
# 假設(shè)你已經(jīng)安裝了某個(gè)支持工具調(diào)用的SDK,例如 together 或 openai
# pip install together # 或 pip install openai
import os
import json
from pathlib import Path # 用于處理文件路徑
# 這里的 client 只是一個(gè)占位符,你需要用實(shí)際的LLM客戶端替換
# 例如: from together import Together; client = Together()
# 或者: from openai import OpenAI; client = OpenAI()
# 請(qǐng)確保 client 對(duì)象支持 chat.completions.create 方法并能處理 tools 參數(shù)
class MockLLMClient:
def chat(self):
class Completions:
def create(self, model, messages, tools=None, tool_choice="auto"):
print("\n--- Calling Mock LLM ---")
print("Messages:", messages)
print("Tools provided:", [t['function']['name'] for t in tools] if tools else"None")
print("-----------------------")
# 在實(shí)際應(yīng)用中,這里會(huì)調(diào)用真實(shí)的API并返回模型響應(yīng)
# 模擬一個(gè)簡(jiǎn)單的工具調(diào)用響應(yīng)
last_user_message = None
for msg in reversed(messages):
if msg['role'] == 'user':
last_user_message = msg['content']
break
if last_user_message:
if"Read the file secret.txt"in last_user_message and tools:
# 模擬模型決定調(diào)用 read_file 工具
return MockResponse(tool_calls=[MockToolCall("read_file", '{"path": "secret.txt"}')])
elif"list files"in last_user_message and tools:
# 模擬模型決定調(diào)用 list_files 工具
return MockResponse(tool_calls=[MockToolCall("list_files", '{}')])
elif"Create a congrats.py script"in last_user_message and tools:
# 模擬模型決定調(diào)用 edit_file 工具
# 這是一個(gè)簡(jiǎn)化的模擬,實(shí)際模型會(huì)解析出路徑和內(nèi)容
args = {
"path": "congrats.py",
"old_str": "",
"new_str": "print('Hello, AI Agent!')\n# Placeholder for rot13 code"
}
return MockResponse(tool_calls=[MockToolCall("edit_file", json.dumps(args))])
# 模擬一個(gè)處理完工具結(jié)果后的回復(fù)
if messages and messages[-1]['role'] == 'tool':
tool_result = messages[-1]['content']
# 需要往前查找對(duì)應(yīng)的assistant/tool_calls消息來(lái)判斷是哪個(gè)工具
tool_call_msg_index = -2# 通常在倒數(shù)第二個(gè)
while tool_call_msg_index >= 0and messages[tool_call_msg_index].get('role') != 'assistant':
tool_call_msg_index -= 1
if tool_call_msg_index >= 0and messages[tool_call_msg_index].get('tool_calls'):
called_tool_name = messages[tool_call_msg_index]['tool_calls'][0]['function']['name'] # 簡(jiǎn)化處理,假設(shè)只有一個(gè)工具調(diào)用
if called_tool_name == 'read_file':
return MockResponse(cnotallow=f"OK,文件內(nèi)容已讀到:{tool_result}")
elif called_tool_name == 'list_files':
return MockResponse(cnotallow=f"當(dāng)前目錄文件列表:{tool_result}")
elif called_tool_name == 'edit_file':
return MockResponse(cnotallow=f"文件操作完成:{tool_result}")
# 模擬一個(gè)普通回復(fù)
return MockResponse(cnotallow="好的,請(qǐng)繼續(xù)。")
return Completions()
class MockResponse:
def __init__(self, cnotallow=None, tool_calls=None):
self.choices = [MockChoice(cnotallow=content, tool_calls=tool_calls)]
class MockChoice:
def __init__(self, content, tool_calls):
self.message = MockMessage(cnotallow=content, tool_calls=tool_calls)
class MockMessage:
def __init__(self, content, tool_calls):
self.content = content
self.tool_calls = tool_calls
def model_dump(self):# 模擬pydantic的model_dump方法
return {"content": self.content, "tool_calls": self.tool_calls}
class MockToolCall:
def __init__(self, name, arguments):
self.id = "call_" + str(hash(name + arguments)) # 簡(jiǎn)單的模擬ID
self.type = "function"
self.function = MockFunction(name, arguments)
class MockFunction:
def __init__(self, name, arguments):
self.name = name
self.arguments = arguments
# 在實(shí)際使用時(shí),請(qǐng)?zhí)鎿Q為你的LLM客戶端初始化代碼
# client = Together() # 示例 Together AI 客戶端
client = MockLLMClient() # 使用Mock客戶端進(jìn)行演示
注意:上面的??MockLLMClient?
?是為了讓代碼可以直接運(yùn)行而提供的模擬客戶端。在實(shí)際應(yīng)用中,你需要用真實(shí)的大模型SDK客戶端替換它,并確保其支持工具調(diào)用功能。
接下來(lái),定義我們的工具函數(shù)及其Schema:
# 定義文件讀取工具
def read_file(path: str) -> str:
"""
讀取文件的內(nèi)容并作為字符串返回。
Args:
path: 工作目錄中的文件相對(duì)路徑。
Returns:
文件的內(nèi)容字符串。
Raises:
FileNotFoundError: 文件不存在。
PermissionError: 沒有權(quán)限讀取文件。
"""
print(f"Executing tool: read_file with path={path}")
try:
# 為了安全,可以增加路徑校驗(yàn),防止讀取非工作目錄文件
# resolved_path = Path(path).resolve()
# if not resolved_path.is_relative_to(Path(".").resolve()):
# raise PermissionError("Access denied: Path is outside working directory.")
with open(path, 'r', encoding='utf-8') as file:
content = file.read()
return content
except FileNotFoundError:
returnf"錯(cuò)誤:文件 '{path}' 未找到。"
except PermissionError:
returnf"錯(cuò)誤:沒有權(quán)限讀取文件 '{path}'。"
except Exception as e:
returnf"錯(cuò)誤:讀取文件 '{path}' 時(shí)發(fā)生異常: {str(e)}"
read_file_schema = {
'type': 'function',
'function': {'name': 'read_file',
'description': '讀取指定路徑文件的內(nèi)容',
'parameters': {'type': 'object',
'properties': {'path': {'type': 'string',
'description': '要讀取文件的相對(duì)路徑'}},
'required': ['path']}}}
# 定義文件列表工具
def list_files(path: str = "."):
"""
列出指定路徑下的所有文件和目錄。
Args:
path (str): 工作目錄中的目錄相對(duì)路徑。默認(rèn)為當(dāng)前目錄。
Returns:
str: 包含文件和目錄列表的JSON字符串。
"""
print(f"Executing tool: list_files with path={path}")
result = []
base_path = Path(path)
ifnot base_path.exists():
return json.dumps({"error": f"路徑 '{path}' 不存在"})
try:
# 為了安全,可以增加路徑校驗(yàn)
# resolved_path = base_path.resolve()
# if not resolved_path.is_relative_to(Path(".").resolve()):
# return json.dumps({"error": "Access denied: Path is outside working directory."})
for entry in base_path.iterdir():
result.append(str(entry)) # 使用str()避免Path對(duì)象序列化問(wèn)題
# 也可以使用 os.walk 更徹底,但這里簡(jiǎn)單起見用 iterdir()
# for root, dirs, files in os.walk(path):
# # ... (類似參考文章的邏輯) ...
# pass # 這里簡(jiǎn)化處理,只列出當(dāng)前目錄
except PermissionError:
return json.dumps({"error": f"沒有權(quán)限訪問(wèn)路徑 '{path}'"})
except Exception as e:
return json.dumps({"error": f"列出文件時(shí)發(fā)生異常: {str(e)}"})
return json.dumps(result)
list_files_schema = {
"type": "function",
"function": {
"name": "list_files",
"description": "列出指定路徑下的所有文件和目錄。",
"parameters": {
"type": "object",
"properties": {
"path": {
"type": "string",
"description": "要列出文件和目錄的相對(duì)路徑。默認(rèn)為當(dāng)前目錄。"
}
}
}
}
}
# 定義文件編輯工具
def edit_file(path: str, old_str: str, new_str: str):
"""
通過(guò)替換字符串來(lái)編輯文件。如果 old_str 為空且文件不存在,則創(chuàng)建新文件并寫入 new_str。
Args:
path (str): 要編輯文件的相對(duì)路徑。
old_str (str): 要被替換的字符串。如果為空,表示創(chuàng)建新文件。
new_str (str): 替換后的字符串。
Returns:
str: "OK" 表示成功,否則返回錯(cuò)誤信息。
"""
print(f"Executing tool: edit_file with path={path}, old_str='{old_str}', new_str='{new_str[:50]}...'") # 打印部分new_str避免過(guò)長(zhǎng)日志
# 為了安全,可以增加路徑校驗(yàn)
# resolved_path = Path(path).resolve()
# if not resolved_path.is_relative_to(Path(".").resolve()):
# return "錯(cuò)誤:拒絕訪問(wèn):路徑超出工作目錄范圍。"
try:
if old_str == ""andnot Path(path).exists():
# 創(chuàng)建新文件
with open(path, 'w', encoding='utf-8') as file:
file.write(new_str)
return"OK: 新文件創(chuàng)建成功。"
else:
# 編輯現(xiàn)有文件
with open(path, 'r', encoding='utf-8') as file:
old_content = file.read()
if old_str == ""and Path(path).exists():
return"錯(cuò)誤:文件已存在,不能用空 old_str 創(chuàng)建。"
if old_str notin old_content and old_str != "":
# 如果指定了 old_str 但未找到,返回錯(cuò)誤
returnf"錯(cuò)誤:文件 '{path}' 中未找到字符串 '{old_str}'。"
new_content = old_content.replace(old_str, new_str)
# 檢查是否真的有內(nèi)容變化(避免無(wú)意義的寫操作)
if old_content == new_content and old_str != "":
# 如果 old_str 不為空但內(nèi)容沒變,說(shuō)明 old_str 沒找到,上面已經(jīng)處理了這個(gè)情況,這里是額外的校驗(yàn)
pass# 應(yīng)該已經(jīng)在上面報(bào)ValueError了,這里保留是為了邏輯清晰
with open(path, 'w', encoding='utf-8') as file:
file.write(new_content)
return"OK: 文件編輯成功。"
except FileNotFoundError:
returnf"錯(cuò)誤:文件未找到: {path}"
except PermissionError:
returnf"錯(cuò)誤:沒有權(quán)限編輯文件: {path}"
except Exception as e:
returnf"錯(cuò)誤:編輯文件 '{path}' 時(shí)發(fā)生異常: {str(e)}"
edit_file_schema = {
"type": "function",
"function": {
"name": "edit_file",
"description": "通過(guò)替換字符串來(lái)編輯文件。如果 old_str 為空且文件不存在,則創(chuàng)建新文件并寫入 new_str。",
"parameters": {
"type": "object",
"properties": {
"path": {
"type": "string",
"description": "要編輯文件的相對(duì)路徑"
},
"old_str": {
"type": "string",
"description": "要被替換的字符串。如果為空且文件不存在,將創(chuàng)建新文件。"
},
"new_str": {
"type": "string",
"description": "替換后的字符串或新文件內(nèi)容。"
}
},
"required": ["path", "old_str", "new_str"]
}
}
}
# 將所有工具的Schema添加到列表中
available_tools = [read_file_schema, list_files_schema, edit_file_schema]
# 創(chuàng)建一個(gè)映射表,將工具名稱映射到實(shí)際函數(shù)
tool_functions = {
"read_file": read_file,
"list_files": list_files,
"edit_file": edit_file,
}
最后,構(gòu)建我們的主循環(huán),處理用戶輸入、調(diào)用大模型、執(zhí)行工具并將結(jié)果反饋:
def chat_with_agent():
messages_history = [{"role": "system", "content": "你是一個(gè)善于使用外部工具來(lái)幫助用戶完成編程任務(wù)的AI助手。當(dāng)你認(rèn)為需要使用工具時(shí),請(qǐng)按照規(guī)范發(fā)起工具調(diào)用。"}]
print("AI編程智能體已啟動(dòng)!輸入指令開始交互 (輸入 'exit' 退出)")
whileTrue:
user_input = input("你: ")
if user_input.lower() in ["exit", "quit", "q"]:
break
messages_history.append({"role": "user", "content": user_input})
# 第一次調(diào)用大模型,讓它決定是否需要工具
try:
response = client.chat.completions.create(
model="Qwen/Qwen2.5-7B-Instruct-Turbo", # 替換為你選擇的模型
messages=messages_history,
tools=available_tools, # 將工具Schema傳遞給模型
tool_choice="auto", # 允許模型自動(dòng)選擇是否使用工具
)
except Exception as e:
print(f"調(diào)用大模型API時(shí)發(fā)生錯(cuò)誤: {e}")
continue# 跳過(guò)當(dāng)前循環(huán),等待用戶輸入
# 檢查大模型是否發(fā)起了工具調(diào)用
response_message = response.choices[0].message
tool_calls = response_message.tool_calls
if tool_calls:
# 如果模型發(fā)起了工具調(diào)用,執(zhí)行工具
print("\n--- 接收到工具調(diào)用指令 ---")
# 將模型的回復(fù)(包含工具調(diào)用信息)添加到歷史中
# 注意:某些API可能在tool_calls的同時(shí)有content,但通常role是assistant
messages_history.append({"role": "assistant", "tool_calls": tool_calls})
for tool_call in tool_calls:
function_name = tool_call.function.name
function_args = json.loads(tool_call.function.arguments)
tool_call_id = tool_call.id
if function_name in tool_functions:
# 查找并執(zhí)行對(duì)應(yīng)的工具函數(shù)
print(f"--> 執(zhí)行工具: {function_name},參數(shù): {function_args}")
try:
# 調(diào)用實(shí)際函數(shù)
# 使用 **function_args 將字典解包作為函數(shù)參數(shù)
function_response = tool_functions[function_name](**function_args)
print(f"<-- 工具執(zhí)行結(jié)果: {str(function_response)[:100]}...") # 打印部分結(jié)果
except Exception as e:
function_response = f"工具執(zhí)行失敗: {e}"
print(f"<-- 工具執(zhí)行結(jié)果: {function_response}")
# 將工具執(zhí)行結(jié)果添加到歷史中,再次調(diào)用大模型
messages_history.append(
{
"tool_call_id": tool_call_id,
"role": "tool",
"name": function_name,
"content": str(function_response), # 工具結(jié)果通常作為字符串傳回
}
)
else:
# 模型調(diào)用了不存在的工具
error_message = f"大模型嘗試調(diào)用未知工具: {function_name}"
print(error_message)
messages_history.append(
{
"tool_call_id": tool_call_id,
"role": "tool",
"name": function_name, # 反饋錯(cuò)誤的工具名
"content": error_message, # 反饋錯(cuò)誤信息
}
)
# 第二次調(diào)用大模型,讓它根據(jù)工具執(zhí)行結(jié)果生成最終回復(fù)
try:
print("\n--- 將工具結(jié)果反饋給大模型,獲取最終回復(fù) ---")
second_response = client.chat.completions.create(
model="Qwen/Qwen2.5-7B-Instruct-Turbo", # 替換為你選擇的模型
messages=messages_history,
)
print("------------------------------------------")
final_response_message = second_response.choices[0].message.content
messages_history.append({"role": "assistant", "content": final_response_message})
print(f"AI: {final_response_message}")
except Exception as e:
print(f"將工具結(jié)果反饋給大模型時(shí)發(fā)生錯(cuò)誤: {e}")
# 如果再次調(diào)用失敗,本次交互就斷了,可以考慮是否需要重試或報(bào)錯(cuò)
else:
# 如果模型沒有調(diào)用工具,直接輸出模型的回復(fù)
assistant_response = response_message.content
messages_history.append({"role": "assistant", "content": assistant_response})
print(f"AI: {assistant_response}")
# 啟動(dòng)智能體
chat_with_agent()
代碼行數(shù)估算:導(dǎo)入部分: ~10-20行 (取決于Mock客戶端的復(fù)雜度) 工具函數(shù)和Schema定義: 3個(gè)工具,每個(gè)工具函數(shù)+Schema大約 30-50行。總共約 90-150行。 工具映射表: ~10行 主循環(huán) ??chat_with_agent?
? 函數(shù): ~100-150行。 總計(jì):約 210 - 330行。
這個(gè)實(shí)現(xiàn)的核心邏輯(工具定義、映射、主循環(huán)解析調(diào)用、執(zhí)行、反饋)確實(shí)可以控制在不到400行代碼內(nèi),甚至更少,完全取決于工具函數(shù)的復(fù)雜度和錯(cuò)誤處理的詳盡程度。這證明了AI Agent的基本框架是相對(duì)簡(jiǎn)潔的。
4. 另一種視角:Anthropic 的 Model Context Protocol (MCP)
我們上面實(shí)現(xiàn)的工具調(diào)用機(jī)制,是目前業(yè)界常用的一種方式,特別是在OpenAI、Together AI以及國(guó)內(nèi)部分大模型API中廣泛采用,通常被稱為 Function Calling (函數(shù)調(diào)用)。它的特點(diǎn)是模型通過(guò)輸出結(jié)構(gòu)化的 JSON 對(duì)象來(lái)表達(dá)工具調(diào)用意圖,外部代碼解析這個(gè)JSON并執(zhí)行對(duì)應(yīng)的函數(shù)。
但除了Function Calling,還有其他重要的Agent與外部世界交互的協(xié)議。其中一個(gè)由Anthropic公司提出的 Model Context Protocol (MCP) 協(xié)議,目前已成為Agent感知外部世界的最受歡迎的協(xié)議之一。
MCP 的核心在于利用上下文(Prompt)中的特定 XML 標(biāo)簽來(lái)構(gòu)建 Agent 與環(huán)境的交互:
- Agent 的“行動(dòng)”:Agent 想要執(zhí)行某個(gè)操作(比如運(yùn)行一段代碼,進(jìn)行搜索),它會(huì)在輸出中生成一段包含在特定標(biāo)簽(例如?
?<tool_code>...</tool_code>?
?? 或??<search_query>...</search_query>?
?) 內(nèi)的文本,這段文本就是給外部執(zhí)行器的指令(比如要運(yùn)行的代碼)。 - 外部的“感知”:外部系統(tǒng)捕捉到 Agent 輸出的帶標(biāo)簽指令后,執(zhí)行相應(yīng)的操作(比如運(yùn)行?
?<tool_code>?
?? 中的代碼,或執(zhí)行??<search_query>?
? 中的搜索)。 - 環(huán)境的“反饋”:外部系統(tǒng)將執(zhí)行的結(jié)果或獲取到的信息,包裹在另一組標(biāo)簽(例如?
?<tool_results>...</tool_results>?
?? 或??<search_results>...</search_results>?
??,或者??<file_contents>...</file_contents>?
? 來(lái)表示文件內(nèi)容)內(nèi),作為新的對(duì)話輪次添加回模型的輸入上下文。
MCP 的優(yōu)勢(shì)在于其靈活性和對(duì)多模態(tài)、多類型信息的整合能力。 通過(guò)不同的標(biāo)簽,可以將代碼執(zhí)行結(jié)果、文件內(nèi)容、網(wǎng)頁(yè)搜索結(jié)果、數(shù)據(jù)庫(kù)查詢結(jié)果等多種形式的外部信息自然地融入到模型的上下文語(yǔ)境中,讓模型能夠“感知”并利用這些豐富的外部信息進(jìn)行推理和決策。這種基于標(biāo)簽的協(xié)議,使得 Agent 能在一個(gè)統(tǒng)一的文本流中協(xié)調(diào)行動(dòng)和感知,尤其適合需要處理和整合多種外部數(shù)據(jù)的復(fù)雜任務(wù)。
對(duì)比來(lái)看:
- 我們代碼中實(shí)現(xiàn)的Function Calling:模型輸出結(jié)構(gòu)化 JSON -> 外部解析 JSON -> 執(zhí)行 -> 將結(jié)果(通常是字符串)作為?
?role: tool?
? 的消息添加回歷史。 - Anthropic 的 MCP:模型輸出帶特定標(biāo)簽的文本 -> 外部解析標(biāo)簽內(nèi)容 -> 執(zhí)行 -> 將結(jié)果帶特定標(biāo)簽的文本作為新的消息添加回歷史。
雖然底層實(shí)現(xiàn)方式不同,但它們都殊途同歸,都是為了讓大模型能夠突破自身的限制,與外部工具和環(huán)境進(jìn)行互動(dòng),從而執(zhí)行更復(fù)雜的任務(wù)。MCP以其在上下文整合上的優(yōu)勢(shì),為 Agent 開啟了感知更廣闊外部世界的大門。
我們代碼中實(shí)現(xiàn)的基于 Function Calling 的方式,是構(gòu)建 Agent 的另一種簡(jiǎn)潔且廣泛支持的途徑,特別適合需要清晰定義和調(diào)用一系列函數(shù)的場(chǎng)景。未來(lái)的 Agent 開發(fā),很可能會(huì)結(jié)合這些不同協(xié)議的優(yōu)點(diǎn),或者出現(xiàn)更高級(jí)的框架來(lái)抽象這些底層交互細(xì)節(jié)。
5. 運(yùn)行你的智能體
要運(yùn)行上面的代碼,你需要:
- 確保安裝了所選LLM提供商的Python SDK(例如?
?pip install together?
?? 或??pip install openai?
?)。 - 將代碼中的?
?MockLLMClient?
?? 替換為你實(shí)際使用的LLM客戶端初始化代碼,并配置好API Key和模型名稱(例如上面的??Qwen/Qwen2.5-7B-Instruct-Turbo?
? 只是一個(gè)示例,請(qǐng)?zhí)鎿Q為你可用的模型)。 - 保存代碼為一個(gè)?
?.py?
??文件,例如??simple_agent.py?
?。 - 可以在代碼同級(jí)目錄下創(chuàng)建一個(gè)?
?secret.txt?
?? 文件,里面寫點(diǎn)內(nèi)容,比如??my secret message is: hello world?
?。 - 在終端運(yùn)行?
?python simple_agent.py?
?。
現(xiàn)在,你可以和你的簡(jiǎn)單AI編程智能體交互了:
$ python simple_agent.py
AI編程智能體已啟動(dòng)!輸入指令開始交互 (輸入 'exit' 退出)
你: list files in the current directory
--- Calling Mock LLM ---
Messages: [{'role': 'system', 'content': '...'}, {'role': 'user', 'content': 'list files in the current directory'}]
Tools provided: ['read_file', 'list_files', 'edit_file']
-----------------------
--- 接收到工具調(diào)用指令 ---
--> 執(zhí)行工具: list_files,參數(shù): {}
<-- 工具執(zhí)行結(jié)果: ["secret.txt", "simple_agent.py"]...
--- 將工具結(jié)果反饋給大模型,獲取最終回復(fù) ---
------------------------------------------
AI: 當(dāng)前目錄文件列表:["secret.txt", "simple_agent.py"] # 這個(gè)回復(fù)是模擬的,實(shí)際取決于你的LLM
你: read the file secret.txt
--- Calling Mock LLM ---
Messages: [{'role': 'system', 'content': '...'}, {'role': 'user', 'content': 'read the file secret.txt'}, {'role': 'assistant', 'tool_calls': [...]}] # 包含之前的工具調(diào)用信息
Tools provided: ['read_file', 'list_files', 'edit_file']
-----------------------
--- 接收到工具調(diào)用指令 ---
--> 執(zhí)行工具: read_file,參數(shù): {'path': 'secret.txt'}
<-- 工具執(zhí)行結(jié)果: my secret message is: hello world...
--- 將工具結(jié)果反饋給大模型,獲取最終回復(fù) ---
------------------------------------------
AI: OK,文件內(nèi)容已讀到:my secret message is: hello world # 這個(gè)回復(fù)是模擬的,實(shí)際取決于你的LLM
你: exit
(注意:使用Mock客戶端時(shí),輸出會(huì)包含Mock的調(diào)試信息,實(shí)際運(yùn)行時(shí)不會(huì)有)
你可以嘗試讓它創(chuàng)建或編輯文件,比如輸入:??create a file named hello.py and put 'print("Hello, Agent!")' inside it?
??。如果你的大模型能理解并正確調(diào)用??edit_file?
?工具,它就會(huì)幫你創(chuàng)建這個(gè)文件。
總結(jié)
通過(guò)這不到400行代碼,我們成功地實(shí)現(xiàn)了一個(gè)具備基本文件操作能力的AI編程智能體。這個(gè)過(guò)程揭示了AI Agent并非遙不可及的黑魔法,它的核心在于:
- LLM的決策能力:能夠理解指令并規(guī)劃如何使用工具。
- 工具的定義:賦予LLM感知和行動(dòng)的能力。
- 調(diào)度層的編排:負(fù)責(zé)解析LLM意圖、執(zhí)行工具并將結(jié)果反饋,形成智能體的循環(huán)。
我們重點(diǎn)講解了基于 Function Calling 的工具調(diào)用機(jī)制,并通過(guò)不到400行代碼的代碼展示了如何實(shí)現(xiàn)這一機(jī)制來(lái)構(gòu)建一個(gè)簡(jiǎn)單的AI編程智能體。同時(shí),我們也簡(jiǎn)要介紹了 Anthropic 提出的基于標(biāo)簽的 Model Context Protocol (MCP),這是另一種強(qiáng)大且流行的 Agent 與外部世界交互協(xié)議,尤其擅長(zhǎng)整合豐富的上下文信息。
希望這篇文章能幫助你理解AI編程智能體的基本原理和實(shí)現(xiàn)方式,并激發(fā)你動(dòng)手實(shí)踐的熱情。AI編程領(lǐng)域充滿機(jī)遇,國(guó)內(nèi)的開發(fā)者們完全可以基于大模型的能力,結(jié)合不同的協(xié)議思想,構(gòu)建出服務(wù)于特定場(chǎng)景的創(chuàng)新工具。
趕緊試試這段代碼吧,邁出構(gòu)建你自己的AI Agent的第一步!
參考鏈接
- ??https://ampcode.com/how-to-build-an-agent??
- ??https://docs.together.ai/docs/how-to-build-coding-agents??
本文轉(zhuǎn)載自??非架構(gòu)??,作者:非架構(gòu)
