Claude code课程:工具的使用-3.利用工具强制生成 JSON

223 阅读11分钟

文章翻译自官网github.com/anthropics/…

利用工具强制生成 JSON

学习目标

  • 理解如何使用工具来强制生成结构化响应

  • 利用这种 “技巧” 生成结构化的 JSON

利用工具的一种更有趣的方式是强制 Claude 输出 JSON 等结构化内容。在很多场景下,我们可能希望从 Claude 那里获得标准化的 JSON 响应,例如:提取实体、汇总数据、分析情感等。

一种方法是直接要求 Claude 返回 JSON,但这可能需要额外的工作来从 Claude 返回的长字符串中提取 JSON,或者确保 JSON 符合我们想要的确切格式。

好消息是,每当 Claude 想要使用工具时,它已经会按照我们定义工具时指定的完美结构化格式进行响应。

在上一节课中,我们给了 Claude 一个计算器工具。当它想要使用这个工具时,会返回类似这样的内容:

json

{
    'operand1': 1984135, 
    'operand2': 9343116, 
    'operation': 'multiply'
}

这看起来和 JSON 非常相似!

如果我们希望 Claude 生成结构化的 JSON,我们可以利用这一点。我们要做的就是定义一个描述特定 JSON 结构的工具,然后告诉 Claude 这个工具。就这样。Claude 会做出响应,它认为自己在 “调用工具”,但实际上我们只关心它给出的结构化响应。

概念概述

这与我们在上一节课中所做的有何不同?以下是上一节课的工作流程图:

image.png

在上一节课中,我们给了 Claude 使用工具的权限,当 Claude 想要调用它时,我们实际上会调用底层的工具函数。

在本节课中,我们将通过告诉 Claude 某个特定工具来 “欺骗” 它,但我们不需要实际调用底层的工具函数。我们使用该工具来强制生成特定结构的响应,如下图所示:

image.png

情感分析

让我们从一个简单的例子开始。假设我们希望 Claude 分析某些文本中的情感,并返回具有以下格式的 JSON 对象:

json

{
  "negative_score": 0.6,
  "neutral_score": 0.3,
  "positive_score": 0.1
}

我们只需要定义一个使用 JSON Schema 来捕获这种结构的工具。以下是一个可能的实现:

python

运行

tools = [
    {
        "name": "print_sentiment_scores",
        "description": "Prints the sentiment scores of a given text.",
        "input_schema": {
            "type": "object",
            "properties": {
                "positive_score": {"type": "number", "description": "The positive sentiment score, ranging from 0.0 to 1.0."},
                "negative_score": {"type": "number", "description": "The negative sentiment score, ranging from 0.0 to 1.0."},
                "neutral_score": {"type": "number", "description": "The neutral sentiment score, ranging from 0.0 to 1.0."}
            },
            "required": ["positive_score", "negative_score", "neutral_score"]
        }
    }
]

现在我们可以告诉 Claude 这个工具,并明确告诉 Claude 使用它,以确保它确实会使用。我们应该会得到一个响应,表明 Claude 想要使用一个工具。工具使用响应应该包含我们想要的确切格式的所有数据。

python

运行

from anthropic import Anthropic
from dotenv import load_dotenv
import json

load_dotenv()
client = Anthropic()

tweet = "I'm a HUGE hater of pickles.  I actually despise pickles.  They are garbage."

query = f"""
<text>
{tweet}
</text>

Only use the print_sentiment_scores tool.
"""

response = client.messages.create(
    model="claude-3-sonnet-20240229",
    max_tokens=4096,
    tools=tools,
    messages=[{"role": "user", "content": query}]
)
response

plaintext

ToolsBetaMessage(id='msg_01BhF4TkK8vDM6z5m4FNGRnB', content=[TextBlock(text='Here is the sentiment analysis for the given text:', type='text'), ToolUseBlock(id='toolu_01Mt1an3KHEz5RduZRUUuTWz', input={'positive_score': 0.0, 'negative_score': 0.791, 'neutral_score': 0.209}, name='print_sentiment_scores', type='tool_use')], model='claude-3-sonnet-20240229', role='assistant', stop_reason='tool_use', stop_sequence=None, type='message', usage=Usage(input_tokens=374, output_tokens=112))

让我们看看我们从 Claude 那里得到的响应。我们将重要部分加粗:

plaintext

ToolsBetaMessage(id='msg_01BhF4TkK8vDM6z5m4FNGRnB', content=[TextBlock(text='Here is the sentiment analysis for the given text:', type='text'), ToolUseBlock(id='toolu_01Mt1an3KHEz5RduZRUUuTWz', input={'positive_score': 0.0, 'negative_score': 0.791, 'neutral_score': 0.209}, name='print_sentiment_scores', type='tool_use')], model='claude-3-sonnet-20240229', role='assistant', stop_reason='tool_use', stop_sequence=None, type='message', usage=Usage(input_tokens=374, output_tokens=112))

Claude “认为” 它在调用一个将使用这种情感分析数据的工具,但实际上我们只是要提取这些数据并将其转换为 JSON:

python

运行

import json
json_sentiment = None
for content in response.content:
    if content.type == "tool_use" and content.name == "print_sentiment_scores":
        json_sentiment = content.input
        break

if json_sentiment:
    print("Sentiment Analysis (JSON):")
    print(json.dumps(json_sentiment, indent=2))
else:
    print("No sentiment analysis found in the response.")

plaintext

Sentiment Analysis (JSON):
{
  "positive_score": 0.0,
  "negative_score": 0.791,
  "neutral_score": 0.209
}

它奏效了!现在让我们把它变成一个可重用的函数,该函数接收一条推文或一篇文章,然后以 JSON 格式打印或返回情感分析结果。

python

运行

def analyze_sentiment(content):

    query = f"""
    <text>
    {content}
    </text>

    Only use the print_sentiment_scores tool.
    """

    response = client.messages.create(
        model="claude-3-sonnet-20240229",
        max_tokens=4096,
        tools=tools,
        messages=[{"role": "user", "content": query}]
    )

    json_sentiment = None
    for content in response.content:
        if content.type == "tool_use" and content.name == "print_sentiment_scores":
            json_sentiment = content.input
            break

    if json_sentiment:
        print("Sentiment Analysis (JSON):")
        print(json.dumps(json_sentiment, indent=2))
    else:
        print("No sentiment analysis found in the response.")

python

运行

analyze_sentiment("OMG I absolutely love taking bubble baths soooo much!!!!")

plaintext

Sentiment Analysis (JSON):
{
  "positive_score": 0.8,
  "negative_score": 0.0,
  "neutral_score": 0.2
}

python

运行

analyze_sentiment("Honestly I have no opinion on taking baths")

plaintext

Sentiment Analysis (JSON):
{
  "positive_score": 0.056,
  "negative_score": 0.065,
  "neutral_score": 0.879
}

使用 tool_choice 强制工具使用

目前,我们通过提示来 “强制” Claude 使用我们的 print_sentiment_scores 工具。在我们的提示中,我们写道 “Only use the print_sentiment_scores tool.(只使用 print_sentiment_scores 工具。)”,这通常是有效的,但有一种更好的方法!我们实际上可以使用 tool_choice 参数强制 Claude 使用特定的工具:

python

运行

tool_choice={"type": "tool", "name": "print_sentiment_scores"}

上面的代码告诉 Claude,它必须通过调用 print_sentiment_scores 工具来响应。让我们更新我们的函数以使用它:

python

运行

def analyze_sentiment(content):

    query = f"""
    <text>
    {content}
    </text>

    Only use the print_sentiment_scores tool.
    """

    response = client.messages.create(
        model="claude-3-sonnet-20240229",
        max_tokens=4096,
        tools=tools,
        tool_choice={"type": "tool", "name": "print_sentiment_scores"},
        messages=[{"role": "user", "content": query}]
    )

    json_sentiment = None
    for content in response.content:
        if content.type == "tool_use" and content.name == "print_sentiment_scores":
            json_sentiment = content.input
            break

    if json_sentiment:
        print("Sentiment Analysis (JSON):")
        print(json.dumps(json_sentiment, indent=2))
    else:
        print("No sentiment analysis found in the response.")

我们将在接下来的课程中更详细地介绍 tool_choice。

实体提取示例

让我们使用相同的方法让 Claude 生成格式良好的 JSON,其中包含从文本样本中提取的人员、组织和位置等实体:

python

运行

tools = [
    {
        "name": "print_entities",
        "description": "Prints extract named entities.",
        "input_schema": {
            "type": "object",
            "properties": {
                "entities": {
                    "type": "array",
                    "items": {
                        "type": "object",
                        "properties": {
                            "name": {"type": "string", "description": "The extracted entity name."},
                            "type": {"type": "string", "description": "The entity type (e.g., PERSON, ORGANIZATION, LOCATION)."},
                            "context": {"type": "string", "description": "The context in which the entity appears in the text."}
                        },
                        "required": ["name", "type", "context"]
                    }
                }
            },
            "required": ["entities"]
        }
    }
]

text = "John works at Google in New York. He met with Sarah, the CEO of Acme Inc., last week in San Francisco."

query = f"""
<document>
{text}
</document>

Use the print_entities tool.
"""

response = client.messages.create(
    model="claude-3-sonnet-20240229",
    max_tokens=4096,
    tools=tools,
    messages=[{"role": "user", "content": query}]
)

json_entities = None
for content in response.content:
    if content.type == "tool_use" and content.name == "print_entities":
        json_entities = content.input
        break

if json_entities:
    print("Extracted Entities (JSON):")
    print(json.dumps(json_entities, indent=2))
else:
    print("No entities found in the response.")

plaintext

Extracted Entities (JSON):
{
  "entities": [
    {
      "name": "John",
      "type": "PERSON",
      "context": "John works at Google in New York."
    },
    {
      "name": "Google",
      "type": "ORGANIZATION",
      "context": "John works at Google in New York."
    },
    {
      "name": "New York",
      "type": "LOCATION",
      "context": "John works at Google in New York."
    },
    {
      "name": "Sarah",
      "type": "PERSON",
      "context": "He met with Sarah, the CEO of Acme Inc., last week in San Francisco."
    },
    {
      "name": "Acme Inc.",
      "type": "ORGANIZATION",
      "context": "He met with Sarah, the CEO of Acme Inc., last week in San Francisco."
    },
    {
      "name": "San Francisco",
      "type": "LOCATION",
      "context": "He met with Sarah, the CEO of Acme Inc., last week in San Francisco."
    }
  ]
}

转存失败,建议直接上传图片文件

我们使用了与之前相同的 “技巧”。我们告诉 Claude 它可以使用一个工具,以此让 Claude 返回特定的数据格式。然后我们提取 Claude 返回的格式化数据,这样就大功告成了。

请记住,在这种用例中,明确告诉 Claude 我们希望它使用某个特定的工具会很有帮助:

Use the print_entities tool.(使用 print_entities 工具。)

写在中间

我最近正在使用一个超nice的Claude code镜像

「超好用的AICODEMIRROR」

真的非常好用,填写我的码「5OTTEB」还能获得额外额度,快来试试!

维基百科摘要示例(包含更复杂的数据)

让我们尝试另一个更复杂的例子。我们将使用 Python 的wikipedia包获取完整的维基百科页面文章,并将其传递给 Claude。我们会让 Claude 生成包含以下内容的响应:

  • 文章的主题

  • 文章的摘要

  • 文章中提到的关键词和主题列表

  • 文章的分类列表(娱乐、政治、商业等)以及分类分数(即主题属于该类别的强度)

如果我们向 Claude 传递关于华特・迪士尼的维基百科文章,可能会得到这样的结果:

json

{
  "subject": "华特·迪士尼",
  "summary": "华特·埃利亚斯·迪士尼是美国动画师、电影制片人和企业家。他是美国动画产业的先驱,在卡通制作方面推出了多项创新。他保持着个人获得奥斯卡奖和提名次数最多的纪录。他还参与了迪士尼乐园和其他主题公园以及电视节目的开发。",
  "keywords": [
    "华特·迪士尼",
    "动画",
    "电影制片人",
    "企业家",
    "迪士尼乐园",
    "主题公园",
    "电视"
  ],
  "categories": [
    {
      "name": "娱乐",
      "score": 0.9
    },
    {
      "name": "商业",
      "score": 0.7
    },
    {
      "name": "科技",
      "score": 0.6
    }
  ]
}

下面是一个函数的示例实现,该函数接收一个维基百科页面主题,查找文章、下载内容、将其传递给 Claude,然后打印生成的 JSON 数据。我们使用相同的策略:通过定义工具来 “引导” Claude 的响应格式。

注意:如果你的电脑上没有安装wikipedia,请确保运行pip install wikipedia进行安装!

python

运行

import wikipedia

# 工具定义
tools = [
    {
        "name": "print_article_classification",
        "description": "打印分类结果。",
        "input_schema": {
            "type": "object",
            "properties": {
                "subject": {
                    "type": "string",
                    "description": "文章的主题",
                },
                "summary": {
                    "type": "string",
                    "description": "文章的段落摘要"
                },
                "keywords": {
                    "type": "array",
                    "items": {
                        "type": "string",
                        "description": "文章中的关键词和主题列表"
                    }
                },
                "categories": {
                    "type": "array",
                    "items": {
                        "type": "object",
                        "properties": {
                            "name": {"type": "string", "description": "类别的名称。"},
                            "score": {"type": "number", "description": "类别的分类分数,范围从0.0到1.0。"}
                        },
                        "required": ["name", "score"]
                    }
                }
            },
            "required": ["subject", "summary", "keywords", "categories"]
        }
    }
]

# 为给定文章主题生成json的函数
def generate_json_for_article(subject):
    page = wikipedia.page(subject, auto_suggest=True)
    query = f"""
    <document>
    {page.content}
    </document>

    使用print_article_classification工具。示例类别包括政治、体育、科技、娱乐、商业。
    """

    response = client.messages.create(
        model="claude-3-haiku-20240307",
        max_tokens=4096,
        tools=tools,
        messages=[{"role": "user", "content": query}]
    )

    json_classification = None
    for content in response.content:
        if content.type == "tool_use" and content.name == "print_article_classification":
            json_classification = content.input
            break

    if json_classification:
        print("文本分类(JSON):")
        print(json.dumps(json_classification, indent=2))
    else:
        print("在响应中未找到文本分类。")

python

运行

generate_json_for_article("杰夫·高布伦")

plaintext

文本分类(JSON):
{
  "subject": "杰夫·高布伦",
  "summary": "杰弗里·林恩·高布伦是美国演员和音乐家,曾出演一些票房最高的电影,如《侏罗纪公园》和《独立日》。他在电影和电视领域有着漫长而成功的职业生涯,在众多电影和电视节目中担任角色。高布伦还是一位出色的爵士乐音乐家,与他的乐队The Mildred Snitzer Orchestra一起发行了多张专辑。",
  "keywords": [
    "演员",
    "音乐家",
    "《侏罗纪公园》",
    "《独立日》",
    "电影",
    "电视",
    "爵士乐"
  ],
  "categories": [
    {
      "name": "娱乐",
      "score": 0.9
    }
  ]
}

python

运行

generate_json_for_article("章鱼")

plaintext

文本分类(JSON):
{
  "subject": "章鱼",
  "summary": "本文全面概述了章鱼,包括它们的解剖学、生理学、行为、生态学和进化历史。内容涵盖了它们复杂的神经系统、伪装和变色能力、智力以及与人类的关系等主题。",
  "keywords": [
    "章鱼",
    "头足类",
    "软体动物",
    "海洋生物学",
    "动物行为",
    "进化"
  ],
  "categories": [
    {
      "name": "科学",
      "score": 0.9
    },
    {
      "name": "自然",
      "score": 0.8
    }
  ]
}

python

运行

generate_json_for_article("赫伯特·胡佛")

plaintext

文本分类(JSON):
{
  "subject": "赫伯特·胡佛",
  "summary": "本文提供了美国第31任总统赫伯特·胡佛的全面传记。内容包括他的早年生活、作为采矿工程师和人道主义者的职业生涯、大萧条期间的总统任期以及他的总统卸任后的活动。",
  "keywords": [
    "赫伯特·胡佛",
    "大萧条",
    "共和党",
    "美国总统",
    "采矿工程师",
    "比利时救济委员会",
    "美国食品管理局",
    "商务部长",
    "斯穆特-霍利关税法",
    "新政"
  ],
  "categories": [
    {
      "name": "政治",
      "score": 0.9
    },
    {
      "name": "商业",
      "score": 0.7
    },
    {
      "name": "历史",
      "score": 0.8
    }
  ]
}

练习

使用上述策略编写一个名为translate的函数,该函数接收一个单词或短语,并生成结构化的 JSON 输出,包含英文原文以及西班牙语、法语、日语和阿拉伯语的翻译。

以下是函数的工作示例:

如果我们调用:

python

运行

translate("how much does this cost")

预期输出如下:

json

{
  "english": "how much does this cost",
  "spanish": "¿cuánto cuesta esto?",
  "french": "combien ça coûte?",
  "japanese": "これはいくらですか",
  "arabic": "كم تكلفة هذا؟"
}

注意:如果你想打印结果,以下代码行将帮助你美观地输出:

python

运行

print(json.dumps(translations_from_claude, ensure_ascii=False, indent=2))