成人免费xxxxx在线视频软件_久久精品久久久_亚洲国产精品久久久_天天色天天色_亚洲人成一区_欧美一级欧美三级在线观看

建議收藏:想做AI編程產(chǎn)品?先從這段不到400行的Agent代碼開始! 精華

發(fā)布于 2025-5-8 06:36
瀏覽
0收藏

前言

去年以來(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è)基本框架:

建議收藏:想做AI編程產(chǎn)品?先從這段不到400行的Agent代碼開始!-AI.x社區(qū)

“感知-決策-行動(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的第一步!

參考鏈接

本文轉(zhuǎn)載自??非架構(gòu)??,作者:非架構(gòu)

標(biāo)簽
已于2025-5-8 09:59:05修改
收藏
回復(fù)
舉報(bào)
回復(fù)
相關(guān)推薦
主站蜘蛛池模板: 午夜寂寞影院在线观看 | 成人国产精品久久 | 亚洲福利在线视频 | 91精品国产色综合久久 | 成人国产精品入口免费视频 | 欧美三区在线观看 | 国产激情一区二区三区 | 黑人巨大精品 | 日韩色视频 | 欧美最猛性xxxxx亚洲精品 | 国产高清在线精品 | 国产在线观看不卡一区二区三区 | 欧美国产精品一区二区 | 91视频进入 | 噜噜噜色网 | 亚洲人a| 久久成人国产精品 | 亚洲精品一区二区久 | 欧美黑人国产人伦爽爽爽 | 先锋资源网 | 中文字幕第一页在线 | 成人一区av | 欧美理论片在线观看 | 国产精品一区一区 | 成人亚洲性情网站www在线观看 | 亚洲三区在线观看 | 色欧美综合 | 久久久精品视频免费 | 欧美精品久久一区 | 美女黄色在线观看 | 国产一区二区三区在线免费观看 | 中文在线一区二区 | 国产在线精品一区二区 | 中国毛片免费 | 日本不卡一区 | 国产日韩欧美 | 国产第一亚洲 | 欧美一区二区三区久久精品 | 亚洲欧美日韩精品久久亚洲区 | 台湾a级理论片在线观看 | 91精品国产91久久综合桃花 |