DLAI-MCP-服务器人工智能应用构建笔记-全-

DLAI MCP 服务器人工智能应用构建笔记(全)

001:1.引言 🧠

在本节课中,我们将学习如何使用模型上下文协议(MCP)服务器来构建AI应用。我们将以处理存储在Box中的非结构化PDF发票为例,演示如何提取关键信息(如客户名称、发票金额和产品名称)并存入数据库。本课程是与Box合作开发的,旨在展示使用MCP服务器的最佳实践。

课程概述

我们将从构建一个自定义解决方案开始,手动读取本地存储的发票并提取文本。然后,我们将利用Box MCP服务器来简化这一流程。最后,我们会将应用扩展为一个多智能体系统,其中每个智能体都可以使用MCP服务器来搜索或处理文件。

什么是MCP?

MCP,即模型上下文协议,它标准化了向基于大语言模型(LLM)的应用提供上下文的方式。这意味着,你无需在AI应用内部编写与外部系统或数据源交互的自定义集成代码,而是可以将你的应用连接到MCP服务器,从而自动扩展LLM可用的工具集。

例如,Box的MCP服务器提供了一套工具,用于与存储在Box中的文件和文件夹进行交互,包括文件搜索、文本提取、基于AI的查询和数据提取。

课程路径

以下是本课程将涵盖的主要步骤:

首先,我们将构建一个自定义解决方案。在这个方案中,你需要手动读取每张本地存储的发票,提取文本,然后将文本传递给应用中的LLM,以从每张发票中提取客户名称、总金额和产品名称。

接下来,我们将通过使用Box MCP服务器来优化流程。该服务器提供了与发票交互和处理所需的所有工具,从而简化了代码。

随着为应用添加更多功能,逻辑可能会变得更加复杂。因此,下一步是将你的应用转变为一个多智能体系统。这个系统由多个专门的智能体组成,每个智能体都可以使用MCP服务器来搜索或处理文件。

例如,一个智能体可以返回文件夹内的文件列表,另一个智能体可以从给定文档中提取数据,或许第三个智能体可以根据发票内容生成最终报告。在本课程中,我们假设这些智能体是独立运行的,甚至可能由其他团队开发。因此,我们将使用谷歌开发的智能体间通信协议(A2A)让它们彼此通信。

致谢

本课程的创建离不开许多人的努力。特别感谢来自Box的Scott Hurrey,以及来自DeepLearning.AI的Har Salami和Eshma Gagari对本课程的贡献。

总结

本节课我们一起介绍了MCP协议的基本概念、本课程的学习目标以及我们将要遵循的实践路径。我们了解到,MCP可以简化AI应用与外部数据的集成,而多智能体架构则能帮助我们管理复杂应用的逻辑。

在下一课中,我们将开始动手实践,首先实现一个不使用Box MCP服务器的AI应用版本,这需要我们为每种文件类型编写自定义代码。

002:构建简单的发票处理应用 💡

在本节课中,我们将学习如何手动处理本地的 PDF 发票文件,提取其文本内容,并使用 Gemini 模型从中提取关键字段信息。


概述

我们将构建一个简单的发票处理应用。整个过程分为几个步骤:首先从本地 PDF 文件中提取文本,然后利用 Gemini 大语言模型从文本中解析出结构化的数据(如客户名称、发票金额、产品名称),最后将这些数据存储到本地 SQLite 数据库中并生成汇总报告。


导入所需库

首先,我们需要导入本教程中要用到的所有库。

import sqlite3
import os
from google import genai
from pypdf import PdfReader
from dotenv import load_dotenv

以下是每个库的作用:

  • sqlite3:用于在本地 SQLite 数据库中存储处理后的信息。
  • genai:Google 的 Gemini 模型客户端库,在本例中作为我们的大语言模型。
  • PdfReader:用于从 PDF 发票中提取文本。
  • dotenv:用于从 .env 文件加载环境变量到当前会话,以便后续访问。

接下来,我们加载环境变量。

load_dotenv()

定义变量与模型

上一节我们导入了必要的库,本节中我们来看看需要定义哪些关键变量。

首先,我选择使用 gemini-2.0-flash 模型。这是一个快速且经济实惠的模型,非常适合我们的示例。

MODEL_NAME = “gemini-2.0-flash”
INVOICE_FOLDER = “./invoices”

同时,我们从环境变量中加载 Gemini 的 API 密钥。

GEMINI_API_KEY = os.getenv(“GEMINI_API_KEY”)

创建 JSON 解析工具函数

有时 Gemini 的响应是 Markdown 格式的 JSON,有时则是原始 JSON。这个辅助函数 parse_json 可以确保无论 Gemini 返回何种格式,我们都能提取出干净的 JSON 数据。

import json
import re

def parse_json(response_text):
    “””
    从模型响应中提取并解析 JSON。
    处理可能包裹在 Markdown 代码块中的 JSON。
    “””
    try:
        # 尝试直接解析
        return json.loads(response_text)
    except json.JSONDecodeError:
        # 如果失败,尝试查找并提取代码块中的 JSON
        match = re.search(r’```(?:json)?\s*({.*?})\s*`’‘, response_text, re.DOTALL)
        if match:
            try:
                return json.loads(match.group(1))
            except json.JSONDecodeError:
                pass
        # 如果都失败,返回空字典或抛出异常
        return {}

注意:作为替代方案,你也可以使用 Pydantic 来定义期望的输出结构,并指示 Gemini 相应地格式化响应。为了保持与下一课代码示例的一致性(下一课同样需要处理模型的工具调用输出),我们在两个示例中都使用了相同的 parse_json 函数。


核心函数:提取发票字段

现在,让我们看看与 Gemini 通信的核心函数。我称这个函数为 extract_invoice_fields

它接收发票文本,将其发送给 Gemini,并要求返回相关的关键字段。

def extract_invoice_fields(invoice_text, filename):
    “””
    使用 Gemini 从发票文本中提取关键字段。
    “””
    # 1. 设置 Gemini 客户端
    client = genai.Client(api_key=GEMINI_API_KEY)

    # 2. 定义提示词
    prompt = f“””
    请从以下发票文本中提取以下字段,并以 JSON 对象形式返回。
    如果找不到某个值,请返回 “null”。

    字段:
    - client_name (客户名称)
    - invoice_amount (发票金额,数字类型)
    - product_name (产品名称)

    发票文本:
    “{invoice_text}”
    “””

    # 3. 调用模型
    response = client.models.generate_content(
        model=MODEL_NAME,
        contents=prompt
    )

    # 4. 解析响应为 JSON
    extracted_data = parse_json(response.text)

    # 5. 添加文件名字段,以便知道数据属于哪张发票
    extracted_data[‘source_file’] = filename

    return extracted_data

这个函数的逻辑很清晰:

  1. 使用 API 密钥设置 Gemini 客户端。
  2. 定义一个直接的提示词,要求 Gemini 从发票文本中提取客户名称、发票金额和产品名称,并以 JSON 对象形式返回信息。
  3. 使用 generate_content 函数调用模型。
  4. 使用之前定义的 parse_json 函数将响应解析为 JSON。
  5. 最后,添加一个 source_file 字段,标明数据来源的文件名。

准备数据存储

我们已经有了提取数据的函数,接下来需要将数据保存起来。为了简单起见,我将创建一个本地数据库。

我选择使用 SQLite。它轻量、快速,且无需任何额外设置。

以下代码创建数据库文件和一个用于存储发票数据的表(如果表不存在的话)。

# 连接到(或创建)SQLite 数据库文件
conn = sqlite3.connect(‘invoices.db’)
cursor = conn.cursor()

# 创建表(如果不存在)
create_table_query = “””
CREATE TABLE IF NOT EXISTS invoices (
    id INTEGER PRIMARY KEY AUTOINCREMENT,
    source_file TEXT UNIQUE,
    client_name TEXT,
    invoice_amount REAL,
    product_name TEXT
);
“””
cursor.execute(create_table_query)
conn.commit()

处理发票文件夹

一切准备就绪,现在开始处理文件夹中的每一张发票。

以下是处理流程:

# 遍历发票文件夹中的每个文件
for filename in os.listdir(INVOICE_FOLDER):
    # 1. 验证文件格式(目前仅支持 PDF)
    if not filename.lower().endswith(‘.pdf’):
        continue

    filepath = os.path.join(INVOICE_FOLDER, filename)
    print(f”正在处理: {filename}“)

    # 2. 从 PDF 提取文本
    full_text = “”
    try:
        reader = PdfReader(filepath)
        for page in reader.pages:
            full_text += page.extract_text() + “\n”
    except Exception as e:
        print(f”读取文件 {filename} 时出错: {e}“)
        continue

    # 3. 调用函数,使用 Gemini 提取结构化数据
    extracted_data = extract_invoice_fields(full_text, filename)

    # 4. 将数据插入数据库
    # 如果发票已处理过,则更新现有条目;否则创建新条目。
    insert_or_update_query = “””
    INSERT OR REPLACE INTO invoices (source_file, client_name, invoice_amount, product_name)
    VALUES (?, ?, ?, ?)
    “””
    cursor.execute(insert_or_update_query,
                   (extracted_data.get(‘source_file’),
                    extracted_data.get(‘client_name’),
                    extracted_data.get(‘invoice_amount’),
                    extracted_data.get(‘product_name’)))
    conn.commit()

# 关闭数据库连接
conn.close()

我们对文件夹中的每张发票都执行上述操作。


生成报告

处理完所有发票后,数据都已存储。现在我们可以生成两份报告。

第一份报告显示发票的总数以及所有发票的总金额。

conn = sqlite3.connect(‘invoices.db’)
cursor = conn.cursor()

# 报告1:总计
cursor.execute(“SELECT COUNT(*), SUM(invoice_amount) FROM invoices WHERE invoice_amount IS NOT NULL”)
total_count, total_amount = cursor.fetchone()
print(“=== 发票汇总报告 ==”)
print(f”发票总数: {total_count}“)
print(f”总金额: ${total_amount or 0:.2f}“)
print()

第二份报告按客户对发票进行分类,并显示从每个客户产生的收入。

# 报告2:按客户细分
print(“=== 按客户细分 ==”)
cursor.execute(“””
    SELECT client_name,
           COUNT(*) as invoice_count,
           SUM(invoice_amount) as total_revenue
    FROM invoices
    WHERE client_name IS NOT NULL AND invoice_amount IS NOT NULL
    GROUP BY client_name
    ORDER BY total_revenue DESC
“””)
for row in cursor.fetchall():
    client, count, revenue = row
    print(f”客户 ‘{client}‘: {count} 张发票,总收入 ${revenue or 0:.2f}“)

conn.close()

运行结果示例如下:

=== 发票汇总报告 ===
发票总数: 5
总金额: $58475.99

=== 按客户细分 ===
客户 ‘Acme Corp’: 2 张发票,总收入 $25000.00
客户 ‘Beta LLC’: 1 张发票,总收入 $15000.00
客户 ‘Gamma Inc’: 1 张发票,总收入 $9999.99
客户 ‘Delta Co’: 1 张发票,总收入 $8476.00

我们可以看到,总共有5张发票,总金额为58475.99美元。按客户细分的报告也反映了相同的信息。


总结

本节课中,我们一起学习并构建了一个简单的发票处理流程。我们通过 PdfReader 提取本地 PDF 文件的文本,利用 Gemini 大语言模型从文本中解析出关键字段,并将这些结构化数据存储到 SQLite 数据库中,最后生成了汇总和细分报告。

在下一节课中,你将学习如何通过使用 Box 的 MCP 服务器来解决同类用例,而不是直接使用 Gemini 并受限于本地文件系统。这将使你的应用能够处理云端存储的文件。

我们下节课再见。

003:3. Box MCP 服务器简介 🧩

在本节课中,我们将学习如何利用 Box MCP 服务器,使你的解决方案变得更加灵活和可扩展。

概述

上一节我们实现了一个概念验证方案。本节中,我们将探讨该方案在实际业务环境中可能遇到的问题,并介绍如何通过模型上下文协议(MCP)和 Box MCP 服务器来解决这些问题,从而构建一个更强大、更通用的解决方案。

现有方案的局限性

你之前实现的解决方案作为概念验证是可行的。但如果尝试在真实的业务环境中使用,会遇到一些问题。

核心问题是自定义构建的解决方案难以扩展。

以下是具体原因:

  • 性能与扩展性问题:在上一课中,文件需要预先下载到本地。但在处理前下载 Box 上的每一张发票速度极慢,无法扩展到处理成千上万的文件。
  • 格式兼容性问题:你构建的脚本是为了处理特定的发票格式。现实中,发票有各种类型,如 Word 文档、PDF、JPEG、PNG 等。你当前的代码需要大量更新才能处理这种多样性。
  • 维护成本高:一旦业务需要提取额外的字段,你就需要重新审视并更新代码。这使得解决方案难以维护。
  • 安全与最佳实践:像这样处理文件需要审查最佳实践和数据安全性,因为存储的文件通常包含敏感数据,这会使此类解决方案变得复杂。

综上所述,这使得该方案超出了简单概念验证的范围,变得不切实际。为了让你的解决方案能够在整个企业内无缝处理各种文件类型,你需要对其进行增强。

引入模型上下文协议(MCP)

这正是模型上下文协议(MCP)的用武之地。MCP 将帮助你简化应用程序的复杂性,使其更加灵活和可扩展。

MCP 是一个标准,定义了如何向 AI 应用程序提供外部工具和数据资源的上下文。

目前,你的解决方案是使用 Gemini 自定义构建的。你必须编写一次性的自定义应用程序来处理发票。为了扩展功能并访问其他工具或数据源,你需要编写更多的自定义代码。这被称为 M × N 问题:对于 M 个应用程序和 N 个外部数据源或系统,你需要编写 M × N 次集成,这很快就会变得难以管理。

通过添加对 MCP 的支持,你的应用程序可以访问任何同样遵循 MCP 标准的外部工具。

以下是其工作原理:工具的定义和执行被卸载到一个 MCP 服务器。这意味着在你的 AI 应用程序内部,你不再需要编写自定义代码来将模型连接到外部系统。相反,你可以在应用程序中使用一个 MCP 客户端,该客户端连接到 MCP 服务器并发送工具调用请求。同一个服务器可以被任何其他 AI 应用程序使用。这将一切简化为一个更易于管理的 M + N 设置。

通过使用 MCP,你可以解决我们之前发现的许多扩展性和灵活性问题。

回到我们的用例:Box MCP 服务器

回到我们的用例。让我们看一个具体的、实用的实现,它将帮助我们使用 MCP 服务器(如 Box MCP 服务器)来改进解决方案。

我们可以将所需的集成工作外包给几乎任何 AI 智能体框架。在这些示例中,我们将使用 Box MCP 服务器,尽管这种方法几乎可以用于任何 MCP 服务器以及其他类似的用例。这正是 MCP 的根本力量。

我们将为此目的使用 Box 提供的本地 MCP 服务器。本质上,它是一个现成的解决方案,处理与 Box 平台的所有复杂交互,包括数据提取 API(这是 Box AI API 中最受欢迎的部分之一)。不同的供应商会提供不同的 MCP 服务器,其中一些你可以本地或云端访问。

以下是 Box MCP 服务器提供的一些功能:

  • 直接内容访问:它允许 AI 应用程序直接访问、理解并对 Box 中的内容进行操作。主要的应用程序不再需要下载和处理文件,它可以直接要求服务器对内容执行操作。
  • 高效处理能力:服务器从一开始就设计用于处理大量文档。它可以非常高效地执行对文件和文件夹的操作。这解决了之前提到的速度和扩展性问题。
  • 内置工具集:它附带了一系列功能,包括搜索、使用 AI 查询数据以及从文档中提取字段。你无需担心 OCR 代码或为特定字段提示模型,因为这些现在都是你可以调用的预构建函数。
  • 安全性:由于所有操作都在 Box 平台内进行,你可以对文件和整个 AI 生态系统进行安全控制。

这里是 GitHub 仓库,你可以找到用于本地运行 Box MCP 服务器的文件。

你可以在这里看到 Box MCP 服务器提供的所有工具列表,它集成了 Box API 来执行各种操作,例如文件搜索、文本提取、基于 AI 的查询和数据提取。

总结

本节课中,我们一起学习了现有自定义解决方案的局限性,并引入了模型上下文协议(MCP)作为解决扩展性和灵活性问题的标准方法。我们重点介绍了 Box MCP 服务器,它作为一个现成的 MCP 实现,能够高效、安全地处理 Box 平台上的文件操作,并提供了丰富的内置工具。下一节课,我们将使用 Box MCP 服务器来重写解决方案,以替代自定义的 API 代码。

004:使用 Box MCP 服务器处理发票 📄

在本节课中,我们将学习如何使用 Box 的 MCP 服务器来处理发票。我们将构建一个应用程序,该程序能够连接到 Box 云存储,列出其中的发票文件,并利用 AI 工具自动提取关键信息,最后将结果存入数据库并生成报告。

概述

我们将通过以下步骤完成发票处理任务:

  1. 设置环境并加载必要的库。
  2. 配置并连接到本地的 Box MCP 服务器。
  3. 发现并使用 MCP 服务器提供的工具。
  4. 编写核心函数来列出文件并提取数据。
  5. 将所有功能整合到一个主循环中,完成从数据提取到报告生成的完整流程。

现在,让我们开始第一步。

加载库与配置环境

首先,我们需要加载必要的库并配置环境变量。

import os
from dotenv import load_dotenv
# 导入 MCP Python SDK 相关模块
from mcp import ClientSession, StdioServerParameters
from mcp.client.stdio import stdio_client

# 加载环境变量
load_dotenv()

我们导入了 dotenv 来管理环境变量,以及 MCP SDK 中的关键组件:ClientSession 用于初始化客户端与服务器的会话,StdioServerParameters 用于定义服务器参数,stdio_client 则允许我们通过标准输入/输出流来生成和连接到本地 MCP 服务器。

接下来,我们定义一些变量。

# 定义模型和路径
MODEL_NAME = “models/gemini-2.0-flash”
MCP_SERVER_PATH = “./mcp-server-box”  # Box MCP 服务器的本地路径

# 定义我们将要使用的工具名称
TOOLS_TO_USE = [“list_folder_contents”, “ai_extract_freeform”]

![](https://github.com/OpenDocCN/dsai-notes-pt1-zh/raw/master/docs/dlai-mcp-svr-aiapp/img/db79b7df13b758b6ae212fa8761fae00_3.png)

# 从环境变量加载敏感信息
GEMINI_API_KEY = os.getenv(“GEMINI_API_KEY”)
BOX_FOLDER_ID = os.getenv(“BOX_FOLDER_ID”)  # 包含发票的 Box 文件夹 ID

这里,我们指定了使用的 AI 模型(Gemini 2.0 Flash)、本地 MCP 服务器的路径、计划使用的两个工具名称,并从 .env 文件加载了 Gemini API 密钥和 Box 文件夹 ID。

配置 MCP 服务器连接

配置好基础变量后,我们需要创建 Gemini 客户端并设置 MCP 服务器的访问方式。

import google.generativeai as genai

# 创建 Gemini 客户端
genai.configure(api_key=GEMINI_API_KEY)
model = genai.GenerativeModel(MODEL_NAME)

# 定义 MCP 服务器的标准输入/输出配置
server_params = StdioServerParameters(
    command=“uv”,
    args=[“run”, “–directory”, MCP_SERVER_PATH, “mcp-server-box”]
)

我们使用 Gemini API 密钥配置了客户端,并定义了 StdioServerParameters。这个配置指定了运行本地 MCP 服务器的命令(这里使用 uv 工具)和参数,指向了服务器文件所在的目录。

定义辅助函数

在构建完整解决方案之前,我们先定义几个辅助函数。上一节我们配置了环境,本节中我们来看看如何与 MCP 服务器交互并处理数据。

首先,定义一个函数来发现 MCP 服务器提供的工具。

async def get_mcp_tools(session):
    “””
    向 MCP 服务器发送请求,获取可用工具列表,并过滤出我们需要的工具定义。
    “””
    # 发送 list_tools 请求
    response = await session.list_tools()
    available_tools = response.tools

    # 过滤出我们需要的工具
    filtered_tools = [
        tool for tool in available_tools
        if tool.name in TOOLS_TO_USE
    ]
    return filtered_tools

这个函数在应用程序连接到 MCP 服务器后被调用。它向服务器请求可用工具列表,然后根据 TOOLS_TO_USE 列表进行过滤,只返回我们需要的工具定义(包括名称和期望的参数)。这些定义之后会被传递给 Gemini,让模型知道它可以调用哪些工具以及如何调用。

接着,我们定义一个解析 JSON 的辅助函数来清理 Gemini 的响应。

import json

def parse_json_response(response_text):
    “””
    尝试从响应文本中解析 JSON 数据。
    “””
    try:
        # 尝试查找 JSON 代码块
        start = response_text.find(‘{‘)
        end = response_text.rfind(‘}’) + 1
        if start != -1 and end != 0:
            json_str = response_text[start:end]
            return json.loads(json_str)
        else:
            return None
    except json.JSONDecodeError:
        return None

现在,我们定义核心的 generate 函数,它负责处理用户查询并管理工具调用。

async def generate(prompt, tools_definitions, session):
    “””
    向 Gemini 发送提示词和工具定义,并处理可能的工具调用。
    “””
    # 准备聊天会话并发送消息
    chat = model.start_chat()
    response = await chat.send_message_async(
        prompt,
        tools=tools_definitions  # 传入工具定义
    )

    # 检查响应中是否包含工具调用
    if response.candidates[0].content.parts[0].function_call:
        fc = response.candidates[0].content.parts[0].function_call
        tool_name = fc.name
        tool_args = fc.args

        # 通过 MCP 客户端调用工具
        tool_response = await session.call_tool(tool_name, tool_args)

        # 解析并返回工具执行结果
        result_text = tool_response.content[0].text
        parsed_result = parse_json_response(result_text)
        return parsed_result if parsed_result else result_text
    else:
        # 如果没有工具调用,直接返回文本
        return response.text

这个函数接收用户提示词和工具定义。它将提示词和工具定义一起发送给 Gemini。如果 Gemini 决定调用工具,其响应中会包含函数调用信息。此时,函数会通过 MCP 会话 (session) 向 MCP 服务器发送 call_tool 请求,并传入工具名称和参数。服务器执行工具后返回结果,我们再将其解析返回。如果 Gemini 没有调用工具,则直接返回生成的文本。

最后,定义一个专门用于从发票中提取字段的函数。

async def extract_invoice_fields(file_id, session, tools_definitions):
    “””
    请求 AI 从指定的 Box 文件(通过 file_id)中提取客户名、发票金额和产品名。
    “””
    prompt = f”””Extract the following fields from the invoice file with ID ‘{file_id}’:
    - Client Name
    - Invoice Amount
    - Product Name
    Return the result as a JSON object.”””

    extracted_data = await generate(prompt, tools_definitions, session)
    return extracted_data

这个函数构造一个明确的提示词,要求 Gemini 从给定文件 ID 的发票中提取三个特定字段。它调用我们刚才定义的 generate 函数来完成实际工作。

请注意一个关键点:在 generate 函数内部,Gemini 可能会决定调用 Box MCP 服务器提供的 ai_extract_freeform 工具。这意味着文件处理和字段提取是在 Box 服务器端完成的,我们的应用程序无需在本地下载 PDF 文件或进行 OCR 转换。这得益于 MCP 架构,让我们能够直接使用服务提供商(Box)提供的强大功能。

构建完整解决方案

有了以上辅助函数,我们现在可以将它们组合到主程序循环中。上一节我们定义了处理单张发票的函数,本节中我们来看看如何批量处理文件夹中的所有发票并生成报告。

首先,建立数据库连接并创建表。

import sqlite3

# 连接到 SQLite 数据库(如果不存在则创建)
conn = sqlite3.connect(‘invoices.db’)
cursor = conn.cursor()

# 创建 invoices 表(如果不存在)
cursor.execute(‘’‘
    CREATE TABLE IF NOT EXISTS invoices (
        id INTEGER PRIMARY KEY AUTOINCREMENT,
        file_id TEXT,
        client_name TEXT,
        amount REAL,
        product_name TEXT
    )
‘’’)
conn.commit()

接下来是主循环代码。

async def main():
    # 1. 创建 MCP 客户端并连接到 Box 服务器
    async with stdio_client(server_params) as (read_stream, write_stream):
        async with ClientSession(read_stream, write_stream) as session:
            # 初始化会话
            await session.initialize()

            # 2. 发现可用的 MCP 工具
            tools_definitions = await get_mcp_tools(session)

            # 3. 列出 Box 文件夹中的内容
            list_prompt = f”List all files in the Box folder with ID ‘{BOX_FOLDER_ID}’.”
            file_list = await generate(list_prompt, tools_definitions, session)

            # 假设 file_list 是一个包含 ‘items’ 列表的字典,每个 item 有 ‘id’ 和 ‘name’
            invoice_files = file_list.get(‘items’, [])

            # 4. 遍历每个发票文件并提取数据
            for file_item in invoice_files:
                file_id = file_item[‘id’]
                print(f”Processing invoice: {file_item[‘name’]} (ID: {file_id})”)

                extracted_data = await extract_invoice_fields(file_id, session, tools_definitions)

                if extracted_data and isinstance(extracted_data, dict):
                    # 5. 将提取的数据插入数据库
                    cursor.execute(‘’‘
                        INSERT INTO invoices (file_id, client_name, amount, product_name)
                        VALUES (?, ?, ?, ?)
                    ‘’’, (file_id,
                           extracted_data.get(‘client_name’),
                           extracted_data.get(‘invoice_amount’),
                           extracted_data.get(‘product_name’)))
                    conn.commit()
                    print(f”  Extracted: {extracted_data}”)
                else:
                    print(f”  Failed to extract data for {file_id}”)

    # 6. 关闭数据库连接
    conn.close()

# 运行主函数
import asyncio
asyncio.run(main())

以下是主循环中每一步的详细说明:

  1. 建立 MCP 连接:使用 stdio_client 在后台生成 MCP 服务器进程,并建立客户端会话 (ClientSession)。
  2. 发现工具:调用 get_mcp_tools 函数,获取我们需要的两个工具(list_folder_contentsai_extract_freeform)的正式定义。
  3. 列出文件:构造提示词让 Gemini 列出指定 Box 文件夹中的所有文件。Gemini 会识别出可用的 list_folder_contents 工具并调用它,MCP 服务器执行后返回文件列表。
  4. 遍历并提取:对于列表中的每一个文件,调用 extract_invoice_fields 函数。该函数内部的 generate 调用会促使 Gemini 使用 ai_extract_freeform 工具在 Box 端处理文件并提取字段。
  5. 存储数据:将成功提取的字段(客户名、金额、产品名)插入到 SQLite 数据库中。
  6. 清理:处理完所有文件后,关闭数据库连接。

运行此程序后,你可以在控制台看到类似以下的输出,表明 Gemini 客户端成功调用了 MCP 工具:

Processing invoice: invoice_001.pdf (ID: 12345)
  Extracted: {‘client_name’: ‘Acme Corp’, ‘invoice_amount’: 1250.75, ‘product_name’: ‘Web Design Service’}
Processing invoice: invoice_002.pdf (ID: 12346)
  Extracted: {‘client_name’: ‘Globex’, ‘invoice_amount’: 890.00, ‘product_name’: ‘Consulting’}
...

生成报告

数据存入数据库后,我们可以像往常一样生成报告。

# 重新打开数据库连接(如果已关闭)
conn = sqlite3.connect(‘invoices.db’)
cursor = conn.cursor()

print(“\n=== 发票总览报告 ===”)
# 报告1:汇总所有发票的总金额
cursor.execute(‘SELECT SUM(amount) as total_amount FROM invoices’)
total = cursor.fetchone()[0]
print(f”所有发票总金额: ${total:.2f}”)

print(“\n=== 按客户收入细分 ==”)
# 报告2:按客户分组,显示每个客户带来的收入
cursor.execute(‘’‘
    SELECT client_name, SUM(amount) as revenue
    FROM invoices
    GROUP BY client_name
    ORDER BY revenue DESC
‘’’)
for row in cursor.fetchall():
    print(f”  {row[0]}: ${row[1]:.2f}”)

conn.close()

这将生成两份清晰的报告:

  1. 所有发票的总金额。
  2. 按客户分组的收入明细。

总结

在本节课中,我们一起学习了如何使用 Box 的 MCP 服务器构建一个发票处理 AI 应用。我们掌握了以下核心内容:

  • MCP 服务器连接:配置并连接到本地运行的 Box MCP 服务器,使用标准输入/输出流进行通信。
  • 工具发现与调用:通过 MCP 会话发现服务器提供的工具,并将工具定义传递给 Gemini 模型,使模型能够智能地决定何时调用这些工具。
  • 远程文件处理:利用 Box 提供的 ai_extract_freeform 工具,直接在 Box 服务器端处理文档并提取结构化数据,无需在本地下载或转换文件,极大地扩展了应用的能力和可处理文件类型。
  • 完整流程集成:将从工具调用、AI 推理、数据提取到数据库存储和报告生成的步骤整合到一个流畅的自动化流程中。

通过本次实践,你看到了 MCP 架构如何让 AI 应用轻松集成第三方服务的强大功能。在下一节课中,你将学习如何通过多智能体架构进一步扩展此应用的能力。

005:从单智能体到多智能体架构 🏗️

在本节课中,我们将学习如何通过从单智能体架构过渡到多智能体架构,来扩展应用的能力。我们还将了解独立的智能体如何通过智能体间协议进行通信与协作。

概述

上一节我们介绍了基于 MCP 的解决方案。新的基于 MCP 的解决方案比原始方案显著更灵活、更健壮

现在,如果你想扩展解决方案的能力,可以在提取的数据之上开始添加更高级的业务逻辑。

扩展应用能力

随着新功能的增加,应用逻辑会迅速变得复杂。为了有效管理这种复杂性,我们需要改进架构方案。一个简单的脚本不足以清晰、可维护地处理所有新逻辑。

一个更高效的架构方法是过渡到多智能体架构

多智能体架构的优势

以下是采用多智能体架构的几个主要优势:

  • 易于构建和维护:每个小型、专门的智能体都更容易构建、测试和维护。
  • 系统更灵活:由于智能体相互独立,如果找到更好的方法,可以轻松替换其中一个,或者以新方式组合它们来创建不同的解决方案。
  • 独立扩展性:可以根据工作负载独立扩展每个智能体,而无需触及系统其余部分。
  • 促进团队协作:这种分离有助于业务更有效地工作,因为可以让不同的团队负责构建和维护每个智能体。

智能体间协作:A2A 协议

假设你有一组独立的、专门的智能体,每个都由不同的团队开发和维护,并且你想在你的应用中重用这些智能体。这就引出了一个重要问题:这些智能体如何协同工作?

这种多智能体协作可以通过 A2A(Agent-to-Agent)协议实现。

A2A 是一个开放标准,一种通用语言,它允许智能体相互通信、协作和委派任务,无论它们由谁构建或在何处运行。

A2A 协议的目标是创建真正的互操作性。它建立了一种通用语言,使不同的智能体能够有效地相互协作。这至关重要,因为它避免了每个新智能体都需要与生态系统中的其他每个智能体进行自定义集成的局面。

此外,通过该协议,智能体可以发现彼此并了解各自的能力。一个智能体可以询问另一个:“你能做什么?”并获得清晰、标准化的答案。这使得它们能够相互委派任务和请求操作。

最终,A2A 将使我们能够支持复杂的多智能体工作流,以解决任何单个智能体独自工作都无法解决的问题。

应用于发票处理应用

让我们回到发票处理应用。你可以将当前的解决方案分解为多个独立的智能体。

让我们从编排器智能体开始。它的工作是协调每个智能体的工作。一旦编排器智能体收到请求,它可以生成一个计划,并委托给每个可用的智能体来寻找解决方案。

以下是可能涉及的智能体及其职责:

  • 文件智能体:返回文件夹内的文件列表。
  • 提取智能体:从任何给定文档中提取结构化数据。
  • 报告智能体:根据发票内容生成最终报告。
  • 标记智能体:负责对金额超过 1000 美元的发票应用标记。
  • 调度器智能体:如果客户发票逾期,发送提醒。
  • 异常检测智能体:运行异常检测模型,识别与客户典型付款历史相比异常延迟的发票。

每个智能体都将有明确、单一的职责,并使用 A2A 协议协同工作、委派任务,从而创建一个更强大、更灵活的应用。

总结

本节课中,我们一起学习了如何通过采用多智能体架构来提升应用的扩展性和可维护性。我们了解到,将复杂任务分解为多个专门的智能体,并通过 A2A 协议实现它们之间的通信与协作,可以构建出更强大、更灵活的系统。在下一课中,你将使用三个通过 A2A 协议通信的智能体,将你的实现转变为一个多智能体系统。

006:使用多智能体系统处理发票 📄

在本节课中,我们将学习如何构建一个由三个独立智能体协同工作的系统来处理发票。这三个智能体分别是:文件智能体、提取智能体和协调智能体。它们将通过 AdaA 协议进行通信,共同完成从文件列表到数据提取的完整流程。

概述

我们将构建一个多智能体系统,该系统包含三个具有明确职责的智能体:

  1. 文件智能体:负责列出指定文件夹中的所有文件。
  2. 提取智能体:负责从特定文档(如发票)中提取结构化数据。
  3. 协调智能体:负责接收用户请求,分析任务,并将子任务分派给相应的智能体。

所有智能体都符合 MCP 标准,可以轻松连接到任何 MCP 服务器。接下来,我们将从导入必要的库开始。

导入必要的库

首先,我们需要导入构建智能体所需的库。这包括 Google 的 Agent Development Kit (ADK) 以及用于 AdaA 通信的库。

# 导入 Google ADK 用于定义智能体
import google_adk

# 导入 AdaA 协议相关库,用于智能体间通信
from adaa import *

# 从 .env 文件加载环境变量
from dotenv import load_dotenv
load_dotenv()

# 其他可能需要的库
import os
import json

我们选择使用 Gemini 2.5 Pro 模型,它比 Gemini Flash 更强大,虽然稍重且慢一些,但对于我们当前的任务来说是值得的。

定义配置与 MCP 服务器

在定义智能体之前,我们需要设置一些基础配置,例如每个智能体监听的本地端口,以及 MCP 服务器的路径。

# 定义每个智能体将使用的本地端口
FILES_AGENT_PORT = 10024
EXTRACTION_AGENT_PORT = 10025
ORCHESTRATOR_AGENT_PORT = 10026

# 定义 Box 文件夹 ID 和 MCP 服务器路径
BOX_FOLDER_ID = “your_folder_id_here”
MCP_SERVER_PATH = “/path/to/your/mcp/server”

MCP 服务器的配置方式与之前相同,采用标准的输入输出配置来定义启动本地 MCP 服务器所需的命令和参数。

定义智能体

现在,让我们开始定义三个核心智能体。为了保持简单,每个智能体都有单一且明确的职责。

1. 文件智能体

文件智能体的工作是列出给定文件夹中的所有文件 ID。它只被授予访问 MCP 服务器中“列出 Box 文件夹内容”这一个工具的权限。

# 定义文件智能体
files_agent = adk.Agent(
    name=“files_agent”,
    instructions=“你的职责是列出指定 Box 文件夹中的所有文件 ID。”,
    tools=[list_box_folder_tool]  # 仅访问列表工具
)

定义完智能体后,我们需要创建它的“智能体卡片”。这张卡片概述了智能体的职责、运行地址、支持的输入输出格式以及技能,其他智能体(如协调者)将根据这张卡片来判断它是否能处理特定任务。

# 创建文件智能体的智能体卡片
files_agent_card = adk.AgentCard(
    name=“files_agent”,
    endpoint=f“http://localhost:{FILES_AGENT_PORT}”,  # 运行地址
    input_schema={“type”: “object”, “properties”: {“folder_id”: {“type”: “string”}}},
    output_schema={“type”: “array”, “items”: {“type”: “string”}},
    skills=[“list_files”]
)

最后,我们为文件智能体创建一个远程实例变量,以便协调智能体能够与之通信。注意,智能体卡片的 endpoint 将用于此目的。

2. 提取智能体

提取智能体的工作是从 Box 中的文件提取发票数据。我们将指示它提取客户名称、发票金额和产品名称,并以特定的 JSON 格式返回。

# 定义提取智能体
extraction_agent = adk.Agent(
    name=“extraction_agent”,
    instructions=“””
    从指定的发票文件中提取以下信息:
    1. 客户名称 (client_name)
    2. 发票金额 (invoice_amount)
    3. 产品名称 (product_name)
    请以 JSON 格式返回,例如:{“client_name”: “...”, “invoice_amount”: 123.45, “product_name”: “...”}
    “””,
    tools=[ai_extract_tool]  # 仅访问 AI 提取工具
)

同样,我们为它创建智能体卡片,并包含一个“从发票提取数据”的技能,同时提供一些交互示例。

# 创建提取智能体的智能体卡片
extraction_agent_card = adk.AgentCard(
    name=“extraction_agent”,
    endpoint=f“http://localhost:{EXTRACTION_AGENT_PORT}”,
    input_schema={“type”: “object”, “properties”: {“file_id”: {“type”: “string”}}},
    output_schema={
        “type”: “object”,
        “properties”: {
            “client_name”: {“type”: “string”},
            “invoice_amount”: {“type”: “number”},
            “product_name”: {“type”: “string”}
        }
    },
    skills=[“extract_invoice_data”]
)

我们也需要为这个智能体创建远程实例。

3. 协调智能体

协调智能体是将所有部分联系起来的核心。它的指令相对复杂:需要解析用户请求,将其分解为步骤,然后将每个步骤委托给合适的子智能体。

# 定义协调智能体
orchestrator_agent = adk.Agent(
    name=“orchestrator_agent”,
    instructions=“””
    你是一个协调者。请按以下步骤处理用户请求:
    1. 解释用户的请求。
    2. 将请求分解为具体的子任务。
    3. 将每个子任务委托给拥有相应技能的智能体(文件智能体或提取智能体)。
    4. 收集子智能体的响应,并整合成最终答案返回给用户。
    你本身不直接访问任何工具,但你可以指挥其他智能体。
    “””,
    # 协调智能体不直接拥有工具,但拥有对子智能体的访问权限
    subagents=[files_agent_card, extraction_agent_card]
)

最后,我们也为协调智能体定义其智能体卡片。

启动智能体服务器

定义完所有智能体后,我们准备运行它们。我们将定义一些函数来在后台启动每个智能体的服务器。

以下是启动智能体服务器的关键组件:

  • Runner 实例:为每个 ADK 智能体提供执行环境,管理会话内的消息处理、事件生成等。
  • Agent Executor:作为 AdaA 协议和智能体逻辑之间的桥梁,处理请求并使用事件队列通信结果。
  • Request Handler:处理队列,将响应发送给发起查询的用户。
  • Starlette 应用实例:包装请求处理器和智能体卡片,用于运行 AdaA 智能体服务器。
# 启动文件智能体服务器
start_agent_server(files_agent, files_agent_card, port=FILES_AGENT_PORT)
# 启动提取智能体服务器
start_agent_server(extraction_agent, extraction_agent_card, port=EXTRACTION_AGENT_PORT)
# 启动协调智能体服务器
start_agent_server(orchestrator_agent, orchestrator_agent_card, port=ORCHESTRATOR_AGENT_PORT)

执行以上代码后,三个智能体将分别在端口 10024、10025 和 10026 上本地运行。可以等待几秒钟确保线程已启动。

使用 AdaA 客户端进行交互

一切就绪后,我们将创建一个 AdaA 客户端。这个客户端就像一个接口,接收用户请求并将其发送给所连接的智能体(这里我们连接到协调智能体)。它会处理智能体卡片的缓存、以正确格式发送任务、解析响应并返回干净的 JSON 或文本。

# 创建 AdaA 客户端并连接到协调智能体
client = adaa.Client(orchestrator_agent_card.endpoint)

运行多智能体系统

现在来到了有趣的部分:运行整个多智能体系统。

首先,我们让协调智能体列出 Box 文件夹中的文件。尽管协调智能体自身没有直接访问工具来完成此任务,但它知道文件智能体可以,因此它会委托该任务。

# 请求协调智能体列出文件
response = client.send_task({“action”: “list_files”, “folder_id”: BOX_FOLDER_ID})
print(“文件列表:”, response)

协调智能体将任务分派给文件智能体,获取响应并返回。结果将显示文件夹中的五个文件列表。

接下来,我们请求协调智能体从一张特定发票中提取数据。同样,协调智能体会意识到提取智能体可以处理此任务。

# 请求协调智能体提取发票数据
response = client.send_task({“action”: “extract_invoice”, “file_id”: “invoice_123.pdf”})
print(“提取的发票详情:”, response)

协调智能体将请求传递给提取智能体并返回结果。我们将看到提取智能体返回的、包含客户名称、金额和产品名称的 JSON 对象。

总结

在本节课中,我们一起构建并运行了一个功能完整的多智能体系统。我们学习了如何:

  1. 定义三个具有明确职责的智能体:文件智能体、提取智能体和协调智能体。
  2. 为每个智能体创建智能体卡片,以描述其技能和通信端点。
  3. 配置并启动基于 AdaA 协议的智能体服务器,使它们能够在本地运行并相互通信。
  4. 使用 AdaA 客户端 与协调智能体进行交互,并由协调智能体自动将任务委托给合适的子智能体。

这个系统展示了清晰的职责划分、基于工具的能力以及智能体间的有效通信。你可以在此基础上扩展,添加更多智能体或更复杂的任务流程。

007:课程总结 🎉

在本节课中,我们将回顾整个课程的核心要点,总结从简单脚本到复杂架构的演进过程,并理解如何构建强大、可扩展且可维护的 AI 解决方案。

恭喜你完成了这门课程。让我们回顾一下你所学到的关键见解。

首先,你通过构建最简单的解决方案来启动项目。

这始终是一个很好的起点,因为它允许你快速开发一个可行的概念验证,而不会过早地陷入复杂性之中。

在确定我们的第一个脚本无法扩展之后,我们以 Boxes MCP 服务器为例,重新实现了我们的解决方案。

这显著简化了代码,使其更加灵活和可扩展。

这一步的关键经验是关于模型上下文协议(MCP)的优势,以及如何利用它来避免为不同的工具和服务编写复杂的一次性集成代码。

最后,随着我们的需求持续增长,我们讨论了将解决方案分解为多个智能体,并使用 A-to-A 协议来协调它们。

这将允许我们通过将问题分解为更小、独立的协作智能体来管理新的复杂性。

通过遵循这种从简单脚本到更复杂架构的自然演进过程,你可以构建出强大、可扩展且可维护的 AI 解决方案。

感谢你与我一同踏上这段旅程。我迫不及待想看到你接下来会构建出什么。😊


本节课总结

在本节课中,我们一起学习了构建 AI 应用的完整演进路径:

  1. 从简单开始:首先构建最简单的可行方案作为概念验证。
  2. 引入 MCP:当简单方案无法扩展时,利用模型上下文协议(MCP)来简化集成,提升灵活性与可扩展性。
  3. 迈向多智能体架构:面对更复杂的需求,将解决方案分解为多个独立的智能体,并通过 A-to-A 协议进行协调。

遵循这一路径,你可以系统地构建出既强大又易于维护的 AI 应用。

posted @ 2026-03-26 08:12  绝不原创的飞龙  阅读(2)  评论(0)    收藏  举报