子智能体协作

使用 Haiku 作为子 Agent 与 Opus 协同工作

🌐 查看英文原文 | 源码 Notebook


使用 Haiku 作为子 Agent

在本教程中,我们将演示如何使用 Claude 3 Haiku 子 Agent 模型来分析苹果公司 2023 年财务收益报告,从收益发布 PDF 中提取相关信息。然后,我们将使用 Claude 3 Opus 来生成对我们问题的响应,并使用 matplotlib 创建图表来配合其回答。

第一步:设置环境

首先,让我们安装所需的库并设置 Claude API 客户端。

%pip install anthropic IPython PyMuPDF matplotlib
# Import the required libraries
import base64
import io
import os
from concurrent.futures import ThreadPoolExecutor

import fitz
import requests
from anthropic import Anthropic
from PIL import Image

# Set up the Claude API client
client = Anthropic()
MODEL_NAME = "claude-haiku-4-5"

第二步:收集文档并提出问题

在本示例中,我们将使用苹果 2023 财年的所有财务报表,并询问全年的净销售额情况。

# List of Apple's earnings release PDF URLs
pdf_urls = [
    "https://www.apple.com/newsroom/pdfs/fy2023-q4/FY23_Q4_Consolidated_Financial_Statements.pdf",
    "https://www.apple.com/newsroom/pdfs/fy2023-q3/FY23_Q3_Consolidated_Financial_Statements.pdf",
    "https://www.apple.com/newsroom/pdfs/FY23_Q2_Consolidated_Financial_Statements.pdf",
    "https://www.apple.com/newsroom/pdfs/FY23_Q1_Consolidated_Financial_Statements.pdf",
]

# User's question
QUESTION = "How did Apple's net sales change quarter to quarter in the 2023 financial year and what were the key contributors to the changes?"

第三步:下载 PDF 并转换为图像

接下来,我们将定义函数来下载收益发布 PDF 并将它们转换为 base64 编码的 PNG 图像。我们必须这样做,因为这些 PDF 中充满了难以用传统 PDF 解析器解析的表格。将它们转换为图像并传递给 Haiku 会更容易。

download_pdf 函数从给定 URL 下载 PDF 文件并保存到指定文件夹。pdf_to_base64_pngs 函数将 PDF 转换为 base64 编码的 PNG 图像列表。

# Function to download a PDF file from a URL and save it to a specified folder
def download_pdf(url, folder):
    response = requests.get(url, timeout=60)
    if response.status_code == 200:
        file_name = os.path.join(folder, url.split("/")[-1])
        with open(file_name, "wb") as file:
            file.write(response.content)
        return file_name
    else:
        print(f"Failed to download PDF from {url}")
        return None


# Define the function to convert a PDF to a list of base64-encoded PNG images
def pdf_to_base64_pngs(pdf_path, quality=75, max_size=(1024, 1024)):
    # Open the PDF file
    doc = fitz.open(pdf_path)

    base64_encoded_pngs = []

    # Iterate through each page of the PDF
    for page_num in range(doc.page_count):
        # Load the page
        page = doc.load_page(page_num)

        # Render the page as a PNG image
        pix = page.get_pixmap(matrix=fitz.Matrix(300 / 72, 300 / 72))

        # Convert the pixmap to a PIL Image
        image = Image.frombytes("RGB", [pix.width, pix.height], pix.samples)

        # Resize the image if it exceeds the maximum size
        if image.size[0] > max_size[0] or image.size[1] > max_size[1]:
            image.thumbnail(max_size, Image.Resampling.LANCZOS)

        # Convert the PIL Image to base64-encoded PNG
        image_data = io.BytesIO()
        image.save(image_data, format="PNG", optimize=True, quality=quality)
        image_data.seek(0)
        base64_encoded = base64.b64encode(image_data.getvalue()).decode("utf-8")
        base64_encoded_pngs.append(base64_encoded)

    # Close the PDF document
    doc.close()

    return base64_encoded_pngs


# Folder to save the downloaded PDFs
folder = "../images/using_sub_agents"


# Create the directory if it doesn't exist
os.makedirs(folder)

# Download the PDFs concurrently
with ThreadPoolExecutor() as executor:
    pdf_paths = list(executor.map(download_pdf, pdf_urls, [folder] * len(pdf_urls)))

# Remove any None values (failed downloads) from pdf_paths
pdf_paths = [path for path in pdf_paths if path is not None]

我们使用 ThreadPoolExecutor 并发下载 PDF,并将文件路径存储在 pdf_paths 中。

第四步:使用 Opus 为 Haiku 生成特定提示

让我们使用 Opus 作为编排者,让它根据用户提出的问题为每个 Haiku 子 Agent 编写特定的提示。

def generate_haiku_prompt(question):
    messages = [
        {
            "role": "user",
            "content": [
                {
                    "type": "text",
                    "text": f"Based on the following question, please generate a specific prompt for an LLM sub-agent to extract relevant information from an earning's report PDF. Each sub-agent only has access to a single quarter's earnings report. Output only the prompt and nothing else.\n\nQuestion: {question}",
                }
            ],
        }
    ]

    response = client.messages.create(model="claude-opus-4-1", max_tokens=2048, messages=messages)

    return response.content[0].text


haiku_prompt = generate_haiku_prompt(QUESTION)
print(haiku_prompt)
Extract the following information from the Apple earnings report PDF for the quarter:
1. Apple's net sales for the quarter
2. Quarter-over-quarter change in net sales
3. Key product categories, services, or regions that contributed significantly to the change in net sales
4. Any explanations provided for the changes in net sales

Organize the extracted information in a clear, concise format focusing on the key data points and insights related to the change in net sales for the quarter.

第五步:从 PDF 中提取信息

现在,让我们定义问题并使用 Haiku 子 Agent 模型从 PDF 中提取信息。我们将每个模型的信息格式化为整洁定义的 XML 标签集合。

def extract_info(pdf_path, haiku_prompt):
    base64_encoded_pngs = pdf_to_base64_pngs(pdf_path)

    messages = [
        {
            "role": "user",
            "content": [
                *[
                    {
                        "type": "image",
                        "source": {
                            "type": "base64",
                            "media_type": "image/png",
                            "data": base64_encoded_png,
                        },
                    }
                    for base64_encoded_png in base64_encoded_pngs
                ],
                {"type": "text", "text": haiku_prompt},
            ],
        }
    ]

    response = client.messages.create(model="claude-haiku-4-5", max_tokens=2048, messages=messages)

    return response.content[0].text, pdf_path


def process_pdf(pdf_path):
    return extract_info(pdf_path, haiku_prompt)


# Process the PDFs concurrently with Haiku sub-agent models
with ThreadPoolExecutor() as executor:
    extracted_info_list = list(executor.map(process_pdf, pdf_paths))

extracted_info = ""
# Display the extracted information from each model call
for info in extracted_info_list:
    extracted_info += (
        '<info quarter="' + info[1].split("/")[-1].split("_")[1] + '">' + info[0] + "</info>\n"
    )
print(extracted_info)
<info quarter="Q4">According to the condensed consolidated statements of operations, Apple's net sales changed as follows in the 2023 financial year:

Quarter Ended September 30, 2023:
- Total net sales were $89,498 million, up from $90,146 million in the prior year quarter.
- Product sales were $67,184 million and services sales were $22,314 million.

Key contributors to the changes:
- Product sales decreased from $70,958 million in the prior year quarter.
- Services sales increased from $19,188 million in the prior year quarter.

Overall, Apple's total net sales decreased slightly compared to the same quarter in the prior year, driven by a decline in product sales which was partially offset by growth in services sales.</info>
<info quarter="Q3">Based on the financial statements provided, Apple's net sales changed as follows between the quarters in the 2023 financial year:

- Net sales increased from $81,797 million in the three months ended July 1, 2023 to $82,959 million in the three months ended June 25, 2022. This represents a quarter-over-quarter increase of around $1,162 million.

The key contributors to this increase appear to be:

- Products sales increased from $60,584 million to $63,355 million, an increase of around $2,771 million.
- Services sales increased from $21,213 million to $19,604 million, an increase of around $1,609 million.

So the growth in both product sales and services sales contributed to the overall increase in Apple's net sales between these two quarters in the 2023 fiscal year.</info>
<info quarter="Q2">According to the financial statements, Apple's net sales changed significantly between the quarters in the 2023 financial year:

- For the three months ended April 1, 2023, net sales were $94,836 million.
- For the six months ended March 26, 2022, net sales were $97,278 million.

This shows that net sales decreased by around $2,442 million from the prior six-month period to the most recent three-month period.

The key contributors to this change in net sales appear to be:

1. Products sales decreased from $107,560 million in the prior six-month period to $73,929 million in the recent three-month period.
2. Services sales increased from $41,673 million in the prior six-month period to $20,907 million in the recent three-month period.

So the decrease in product sales was the primary driver behind the overall decline in net sales from the prior period to the most recent quarter.</info>
<info quarter="Q1">According to the condensed consolidated statements of operations, Apple's net sales increased from $117,154 million in the three months ended December 31, 2022 to $123,945 million in the three months ended December 25, 2021. This represents a quarter-over-quarter increase in net sales.

The key contributors to this change were:

1. Increase in product sales revenue from $96,388 million to $104,429 million.
2. Increase in services revenue from $20,766 million to $19,516 million.

So the overall increase in net sales was driven primarily by higher product sales, with services revenue also showing a smaller increase quarter-over-quarter.</info>

我们使用子 Agent 模型并发地从 PDF 中提取信息,并合并提取的信息。然后,我们为强大的模型准备消息,包括问题和提取的信息,并要求它生成响应和 matplotlib 代码。

第六步:将信息传递给 Opus 以生成响应

现在我们已经使用子 Agent 从每个 PDF 中获取了信息,让我们调用 Opus 来实际回答问题并编写代码来创建图表以配合答案。

# Prepare the messages for the powerful model
messages = [
    {
        "role": "user",
        "content": [
            {
                "type": "text",
                "text": f"Based on the following extracted information from Apple's earnings releases, please provide a response to the question: {QUESTION}\n\nAlso, please generate Python code using the matplotlib library to accompany your response. Enclose the code within <code> tags.\n\nExtracted Information:\n{extracted_info}",
            }
        ],
    }
]

# Generate the matplotlib code using the powerful model
response = client.messages.create(model="claude-opus-4-1", max_tokens=4096, messages=messages)

generated_response = response.content[0].text
print("Generated Response:")
print(generated_response)
Generated Response:
Based on the extracted information from Apple's earnings releases, Apple's net sales changed as follows in the 2023 financial year:

In Q1, net sales increased from $117,154 million in the previous quarter to $123,945 million, driven by increases in both product sales and services revenue.

In Q2, net sales decreased by around $2,442 million compared to the prior six-month period, primarily due to a decrease in product sales, which was partially offset by an increase in services sales.

In Q3, net sales increased by approximately $1,162 million compared to the previous quarter, with growth in both product sales and services sales contributing to the overall increase.

In Q4, total net sales decreased slightly compared to the same quarter in the prior year, driven by a decline in product sales, which was partially offset by growth in services sales.

Here's a Python code snippet using the matplotlib library to visualize the quarterly net sales data:

<code>
import matplotlib.pyplot as plt

quarters = ['Q1', 'Q2', 'Q3', 'Q4']
net_sales = [123945, 94836, 82959, 89498]

plt.figure(figsize=(8, 6))
plt.bar(quarters, net_sales, color='skyblue', width=0.6)
plt.xlabel('Quarter')
plt.ylabel('Net Sales (in millions)')
plt.title('Apple Net Sales by Quarter - FY 2023')
plt.xticks(quarters)
plt.yticks([0, 20000, 40000, 60000, 80000, 100000, 120000])
plt.grid(True, linestyle='--', alpha=0.5)

for i, v in enumerate(net_sales):
    plt.text(i, v + 1000, f'${v:,.0f}', ha='center', fontsize=10)

plt.show()
</code>

This code creates a bar chart showing Apple's net sales for each quarter in the 2023 financial year. The x-axis represents the quarters, and the y-axis represents the net sales in millions of dollars. The chart also includes data labels showing the exact net sales values for each quarter.

第七步:提取响应并执行 Matplotlib 代码

最后,让我们从生成的响应中提取 matplotlib 代码并执行它,以可视化收入增长趋势。

我们定义 extract_code_and_response 函数来从生成的响应中提取 matplotlib 代码和非代码响应。我们打印非代码响应,如果找到了 matplotlib 代码则执行它。

请注意,在沙箱之外对模型编写的代码使用 exec 不是好的做法,但出于本演示的目的我们这样做 :)

# Extract the matplotlib code from the response
# Function to extract the code and non-code parts from the response
def extract_code_and_response(response):
    start_tag = "<code>"
    end_tag = "</code>"
    start_index = response.find(start_tag)
    end_index = response.find(end_tag)
    if start_index != -1 and end_index != -1:
        code = response[start_index + len(start_tag) : end_index].strip()
        non_code_response = response[:start_index].strip()
        return code, non_code_response
    else:
        return None, response.strip()


matplotlib_code, non_code_response = extract_code_and_response(generated_response)

print(non_code_response)
if matplotlib_code:
    # Execute the extracted matplotlib code
    # Note: exec is used here for demonstration purposes to run model-generated visualization code.
    # In production, use a sandboxed environment for executing untrusted code.
    exec(matplotlib_code)  # noqa: S102
else:
    print("No matplotlib code found in the response.")

Tip

中文读者提示:本章节的代码和输出为英文原文。如需理解具体实现细节,可参考上方中文导读和代码注释。如有疑问,欢迎在 GitHub Issues 讨论。