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