← 文章 / AI技术
freeCodeCamp 1小时前 · 2026-09-30 06:15:48 · 1 阅读

如何使用 Claude API 构建可靠的 AI 助手

大语言模型能够回答问题、总结文档、编写代码,并与外部系统交互。但要构建可靠的 AI 应用,仅靠发送提示词并显示回复是远远不够的。

生产级别的应用需要管理对话历史、提供相关上下文、安全地调用工具、处理不同的响应类型,并评估生成的输出是否有用。

在本教程中,我们将构建一个ShopHelper,它是虚构在线商店的客户支持助手。到最后,ShopHelper 将具备以下能力:

  • 以一致的语气回答一般性问题

  • 记住客户之前说过的话

  • 通过调用代码中的函数来查询订单状态

  • 安全地处理 Claude 的多块(multi-block)响应

  • 使用工作流(workflow)处理支持工单

  • 评估提示词修改是否提升了结果

每一节都会增加一个功能模块,方便你在自己的编辑器中跟随操作。

目录

前置准备

你需要准备:

  • 基础的 Python 知识

  • Python 3.9 或更高版本

  • 一个 Anthropic API 密钥

  • 熟悉函数和 JSON

如何配置项目并保护 API 密钥

创建虚拟环境并安装 Anthropic Python SDK:

python -m venv .venv
source .venv/bin/activate
pip install anthropic python-dotenv

在 Windows 上:

.venv\Scripts\activate

创建 .env 文件:

ANTHROPIC_API_KEY=your_api_key_here

API 密钥是秘密凭证。切勿将其放在浏览器 JavaScript、移动应用代码或客户端配置中。也绝不要将其提交到代码仓库:

echo ".env" >> .gitignore

如果后续添加 Web 界面,请确保密钥保存在后端:

Browser → Your backend → Claude API

创建 app.py:

import os

from anthropic import Anthropic
from dotenv import load_dotenv

load_dotenv()

MODEL = "claude-sonnet-5"

client = Anthropic(
    api_key=os.environ["ANTHROPIC_API_KEY"]
)

load_dotenv() 会从 .env 文件中加载值。使用 MODEL 常量意味着只需在一个地方修改模型名称。在运行示例之前,请确认该模型标识符对你的账号可用。

如何发送第一个请求

response = client.messages.create(
    model=MODEL,
    max_tokens=500,
    messages=[
        {
            "role": "user",
            "content": "Explain what an API is in simple terms."
        }
    ],
)

answer = "".join(
    block.text
    for block in response.content
    if block.type == "text"
)

print(answer)

一个请求包含三个重要部分:

  • model 选择处理该请求的 Claude 模型。不同模型在能力、速度和成本上可能存在差异。

  • max_tokens 限制 Claude 可生成的最大文本量。较小的值可以降低延迟,但 Claude 可能会在未完成回答前停止。

  • messages 包含对话内容。每条消息都有 role 和 content。角色通常是 user 或 assistant。

比如,单次请求只包含一条 user 消息,而多轮对话则包含之前的 user 和 assistant 消息。

Claude 返回的 response.content 是一个由带类型的内容块组成的列表。常见的内容块包括:

内容块类型 含义
text 生成的文本
tool_use 要求你的应用调用某个工具
thinking 开启扩展思考时的推理内容

示例代码会收集所有 text 内容块,而不是直接假定 response.content[0] 一定是文本。

你可以查看用量信息来做监控:

print(response.usage.input_tokens)
print(response.usage.output_tokens)

如何管理对话历史

Claude 不会自动记住各次独立的 API 请求。你需要在每次请求时把相关的历史记录一起发送:

messages = [
    {
        "role": "user",
        "content": "What is your returns policy?"
    },
    {
        "role": "assistant",
        "content": "Items can be returned within 30 days."
    },
    {
        "role": "user",
        "content": "How long do I have?"
    },
]

response = client.messages.create(
    model=MODEL,
    max_tokens=300,
    messages=messages,
)

其中 assistant 消息记录了 Claude 之前的回答,这样最后那句提问就能结合上下文被正确理解。

一个简单的聊天函数可以这样维护历史记录:

def chat(history, user_text):
    history.append({
        "role": "user",
        "content": user_text,
    })

    response = client.messages.create(
        model=MODEL,
        max_tokens=500,
        messages=history,
    )

    reply = "".join(
        block.text
        for block in response.content
        if block.type == "text"
    )

    history.append({
        "role": "assistant",
        "content": reply,
    })

    return reply

history = []

print(chat(history, "What is your returns policy?"))
print(chat(history, "How long do I have?"))

每次调用都会追加新的 user 消息、发送完整的历史记录,并把 Claude 的回复存起来供下一轮使用。在生产环境中,应按客户 ID 或会话 ID 来存储对话历史。

历史记录不断增长时如何处理

无限制的对话历史会增大输入量,反而可能让 Claude 难以集中注意力。一种做法是只保留最近的消息:

def trim_history(history, max_messages=10):
    trimmed = history[-max_messages:]

    while trimmed and trimmed[0]["role"] != "user":
        trimmed.pop(0)

    return trimmed

另一种做法是对较早的轮次进行摘要,同时保留近期的消息:

def summarise_history(history, keep_last=6):
    old = history[:-keep_last]
    recent = history[-keep_last:]

    transcript = "\n".join(
        f"{message['role']}: {message['content']}"
        for message in old
    )

    response = client.messages.create(
        model=MODEL,
        max_tokens=250,
        messages=[{
            "role": "user",
            "content": (
                "Summarise this conversation in under 100 words. "
                "Keep order numbers and unresolved issues.\n\n"
                f"<conversation>{transcript}</conversation>"
            ),
        }],
    )

    summary = "".join(
        block.text
        for block in response.content
        if block.type == "text"
    )

    return summary, recent

请将摘要作为独立的应用状态保存,并在下次请求时作为上下文传入。不要将其插入到 recent 之前作为一条额外的用户消息,因为这会制造出无效的连续用户消息序列。

敏感信息在存储或传输前也应当进行脱敏处理:

import re

def redact(text):
    return re.sub(
        r"\b(?:\d[ -]?){13,16}\b",
        "[REDACTED CARD]",
        text,
    )

如何用清晰边界构建 Prompt 结构

XML 风格的标签本质上是普通文本,并非特殊的 API 指令。它们能让 Prompt 的各个部分更加明确:

prompt = """
<customer_reviews>
The product is comfortable, but the available colours are limited.
Customers also describe it as durable.
</customer_reviews>

<sales_data>
January: 120 units
February: 150 units
March: 98 units
</sales_data>

<task>
Compare the reviews with the sales data.
Identify possible relationships and state uncertainty.
</task>
"""

在此,<customer_reviews> 标识参考材料,<sales_data> 标识数据,<task> 标识指令。对于策略、用户生成内容、示例及输出要求,也请使用类似的边界进行界定。

如何使用 System Prompt

System Prompt 定义了 ShopHelper 的通用行为准则:

system_prompt = """
你是 ShopHelper,一位友善的客服助手。

回答需简洁清晰。
不要虚构价格、政策或订单细节。
如果信息缺失,请主动询问。
"""

在传递时,请将 System Prompt 与会话内容分开:

response = client.messages.create(
    model=MODEL,
    max_tokens=500,
    system=system_prompt,
    messages=[
        {"role": "user", "content": "我的订单在哪里?"}
    ],
)

由于顾客未提供订单号,ShopHelper 应当索要订单号,而不是盲目猜测。

如何添加 Tools

Claude 无法直接访问你的数据库。通过 Tools,它可以以结构化的方式向你的应用请求信息:

def get_order_status(order_id):
    orders = {
        "ORD-1001": "shipped",
        "ORD-1002": "processing",
    }

    return {
        "order_id": order_id,
        "status": orders.get(order_id, "not_found"),
    }

该函数接收订单 ID,执行查询并返回可预期的数据。在生产环境中,字典会被数据库查询替代。Claude 不会执行该函数,而是由你的应用来执行。

使用 schema 描述该函数:

tools = [{
    "name": "get_order_status",
    "description": "获取客户订单的当前状态。",
    "input_schema": {
        "type": "object",
        "properties": {
            "order_id": {
                "type": "string",
                "description": "订单 ID,例如 ORD-1001。"
            }
        },
        "required": ["order_id"],
    },
}]

Claude 可能会返回一个 tool_use 块,而非最终答案:

type="tool_use"
id="toolu_example"
name="get_order_status"
input={"order_id": "ORD-1001"}

name 标识函数名称,input 包含参数,id 用于返回结果时引用。stop_reason 为 "tool_use" 表示你的应用需要先处理这个请求,再让 Claude 继续。

如何处理 tool-use 响应

tool-use 响应就是包含上述 tool_use 块的响应。

在执行前,先校验工具名称、参数和用户权限:

import re

ORDER_ID_PATTERN = re.compile(r"^ORD-\d{4}$")

def validate_tool_request(name, tool_input, current_user):
    if name != "get_order_status":
        return False, "Unknown tool"

    order_id = tool_input.get("order_id")

    if not isinstance(order_id, str):
        return False, "order_id must be a string"

    if not ORDER_ID_PATTERN.fullmatch(order_id):
        return False, "Invalid order ID format"

    if order_id not in current_user["order_ids"]:
        return False, "The customer cannot access this order"

    return True, None

之后用一个完整的循环来校验并执行请求:

def run_conversation(user_text, current_user):
    messages = [{"role": "user", "content": user_text}]

    while True:
        response = client.messages.create(
            model=MODEL,
            max_tokens=500,
            system=system_prompt,
            tools=tools,
            messages=messages,
        )

        if response.stop_reason != "tool_use":
            return "".join(
                block.text
                for block in response.content
                if block.type == "text"
            )

        messages.append({
            "role": "assistant",
            "content": response.content,
        })

        results = []

        for block in response.content:
            if block.type != "tool_use":
                continue

            valid, error = validate_tool_request(
                block.name,
                block.input,
                current_user,
            )

            if valid:
                result = get_order_status(block.input["order_id"])
                results.append({
                    "type": "tool_result",
                    "tool_use_id": block.id,
                    "content": str(result),
                })
            else:
                results.append({
                    "type": "tool_result",
                    "tool_use_id": block.id,
                    "content": error,
                    "is_error": True,
                })

        messages.append({
            "role": "user",
            "content": results,
        })

tool_use_id 用于将执行结果与最初的请求关联起来。应用层仍需负责鉴权和具体的执行逻辑。

Claude 响应可能包含多个块

以下这种假设非常脆弱:

answer = response.content[0].text

它默认第一个块存在且一定是文本类型。正确的做法是遍历并检查每个块:

for block in response.content:
    if block.type == "text":
        print(block.text)
    elif block.type == "tool_use":
        print("Validate and execute:", block.name)
    elif block.type == "thinking":
        continue
    else:
        print("Unhandled block type:", block.type)

在 ShopHelper 中,程序会显示文本内容,校验并执行获批的 Tool 请求,屏蔽内部思维链,同时记录未知的块类型。

Workflow 与 Agent

Workflow 遵循预定义的执行序列:

接收工单
↓
提取详情
↓
起草回复
↓
审核回复
def ask(prompt, max_tokens=500):
    response = client.messages.create(
        model=MODEL,
        max_tokens=max_tokens,
        messages=[{"role": "user", "content": prompt}],
    )

    return "".join(
        block.text
        for block in response.content
        if block.type == "text"
    )

def handle_ticket_workflow(ticket):
    details = ask(
        f"<ticket>{ticket}</ticket>\n"
        "<task>Extract the problem and desired outcome.</task>"
    )

    draft = ask(
        f"<details>{details}</details>\n"
        "<task>Draft a concise support reply.</task>"
    )

    review = ask(
        f"<draft>{draft}</draft>\n"
        "<task>List unsupported promises, or say OK.</task>"
    )

    return draft, review

Agent 的灵活性更高:由 Claude 自行决定何时调用工具以及下一步执行什么操作。不过,Agent 同样需要验证机制和最大步数限制。上述 run_conversation() 函数可以直接复用在 Agent 的循环逻辑中。

如果步骤明确且注重可重复性,选用 Workflow;如果后续动作依赖于当前结果,则选用 Agent。

链式调用、并行化、路由与评估-优化

链式调用将每一步的结果传递给下一阶段:

def chained_reply(ticket, policy):
    draft = ask(
        f"<ticket>{ticket}</ticket>\n"
        "<task>Draft a support reply.</task>"
    )

    issues = ask(
        f"<policy>{policy}</policy>\n"
        f"<draft>{draft}</draft>\n"
        "<task>List unsupported claims.</task>"
    )

    return ask(
        f"<draft>{draft}</draft>\n"
        f"<issues>{issues}</issues>\n"
        "<task>Rewrite the final reply.</task>"
    )

并行化让独立任务并发执行:

from concurrent.futures import ThreadPoolExecutor

tickets = [
    "My headphones arrived broken.",
    "I was charged twice.",
    "How do I change my address?",
]

def summarise(ticket):
    return ask(
        f"<ticket>{ticket}</ticket>\n"
        "<task>Summarise in one sentence.</task>",
        max_tokens=100,
    )

with ThreadPoolExecutor(max_workers=3) as pool:
    summaries = list(pool.map(summarise, tickets))

digest = ask(
    "<summaries>\n"
    + "\n".join(summaries)
    + "\n</summaries>\n"
    "<task>Summarise today's support themes.</task>"
)

路由是先对请求分类,再选择对应的专用流程:

def route(ticket):
    label = ask(
        f"<ticket>{ticket}</ticket>\n"
        "<task>Return exactly refund, delivery, or general.</task>",
        max_tokens=10,
    ).strip().lower()

    return label if label in {"refund", "delivery", "general"} else "general"

评估器-优化器(Evaluator-optimizer)先生成答案,再审查并修订:

def improve_reply(ticket, rounds=2):
    reply = ask(
        f"<ticket>{ticket}</ticket>\n"
        "<task>Write a support reply.</task>"
    )

    for _ in range(rounds):
        review = ask(
            f"<reply>{reply}</reply>\n"
            "<task>List accuracy or tone problems, or say PASS.</task>"
        )

        if review.strip().upper() == "PASS":
            break

        reply = ask(
            f"<reply>{reply}</reply>\n"
            f"<review>{review}</review>\n"
            "<task>Rewrite the reply.</task>"
        )

    return reply

总结一下:各阶段相互依赖时用链接,工作相互独立时用并行化,需要分流处理时用路由,当质量提升值得多花几次 API 调用时用评估器-优化器循环。

如何评估提示词质量

准备有代表性的测试用例:

test_cases = [
    {
        "ticket": "I want a refund for broken headphones.",
        "expected": "refund",
    },
    {
        "ticket": "Where is ORD-1002?",
        "expected": "delivery",
    },
    {
        "ticket": "Do you sell gift cards?",
        "expected": "general",
    },
]

这些用例涵盖了不同类型的请求。在修改系统提示词、示例、模型、令牌限制或路由指令后,应重新运行相同的用例:

def evaluate(route_fn, cases):
    passed = 0

    for case in cases:
        result = route_fn(case["ticket"])

        if result == case["expected"]:
            passed += 1
        else:
            print("Failed:", case["ticket"], result)

    score = passed / len(cases)
    print(f"{passed}/{len(cases)} passed")
    return score

针对标签和 JSON 数据,使用代码式评估器。针对语气、准确性和有用性,使用人工或基于模型的评估器。

结语

基于 Claude API 进行开发,远不止编写提示词那么简单。构建可靠的应用需要结构化的上下文、受控的对话状态、经过验证的工具执行、审慎的响应处理、恰当的工作流以及可重复的评估机制。

目标不是寻找完美的提示词,而是围绕 Claude 构建一套系统,该系统能提供正确的上下文,限制不安全操作,妥善处理不确定性,并衡量变更是否提升了结果。

原始来源: freeCodeCamp

评论 (0)