درس ۵ از ۱۱

Tool Use with Claude

تا اینجای فصل، Claude را به‌عنوان یک «تولیدکننده متن» دیدیم: prompt می‌فرستیم، پاسخ می‌گیریم. اما در عمل، اپلیکیشن‌های جدی نیاز دارند که model بتواند با دنیای بیرون تعامل کند: query بزند به database، فایل را ویرایش کند، در وب جست‌وجو کند، یا یک API خارجی را صدا بزند. ابزار اصلی برای این کار tool use است — قراردادی که Claude را از یک chatbot به یک function caller تبدیل می‌کند.

هدف یادگیری ماژول: تسلط بر کل چرخه‌ی tool use — تعریف tool با JSON schema، parse کردن tool_use block، برگرداندن tool_result، اجرای multi-turn loop، استفاده هم‌زمان از چند tool، fine-grained streaming، و دو tool رسمی Anthropic یعنی text edit tool و web search tool.


۵.۱ Claude می‌تواند function صدا بزند

هدف یادگیری: درک قرارداد tool use — شما tools را تعریف می‌کنید، Claude درخواست فراخوانی می‌دهد، شما اجرا می‌کنید، نتیجه را برمی‌گردانید.

مفاهیم کلیدی: پارامتر tools، content block از نوع tool_use، content block از نوع tool_result، تفاوت client tools و server tools، Anthropic-schema tools (مثل bash، text_editor، computer، memory)، stop_reason: "tool_use"، agentic loop.

به بیان دقیق Anthropic: «Tool use یک قرارداد بین اپلیکیشن شما و model است. شما مشخص می‌کنید چه عملیاتی در دسترس است و input/output آن چه شکلی دارد؛ Claude تصمیم می‌گیرد چه زمانی و چگونه آن را صدا بزند.» Tools سه دسته دارند: user-defined client tools (شما schema می‌نویسید و کد را خودتان اجرا می‌کنید — اکثر موارد)، Anthropic-schema client tools (مثل bash، text_editor، computer، memory — Anthropic schema را منتشر کرده، اما اجرا با شماست)، و server tools (مثل web_search، web_fetch، code_execution، tool_search — Anthropic هم schema می‌دهد و هم اجرا می‌کند).

برای client tools، حلقه بنیادی همیشه یکسان است: tools به‌همراه پیام کاربر می‌فرستید → Claude با stop_reason: "tool_use" پاسخ می‌دهد → کد شما tool را اجرا می‌کند → یک پیام جدید همراه با block از نوع tool_result برمی‌گردانید → این چرخه ادامه دارد تا stop_reason چیز دیگری شود (مثلاً end_turn).

نمونه کد:

weather_tool = {
    "name": "get_weather",
    "description": "Get the current weather for a city.",
    "input_schema": {
        "type": "object",
        "properties": {
            "city": {"type": "string"},
            "unit": {"type": "string", "enum": ["celsius", "fahrenheit"]},
        },
        "required": ["city"],
    },
}

resp = client.messages.create(
    model="claude-opus-4-8",
    max_tokens=1024,
    tools=[weather_tool],
    messages=[{"role": "user", "content": "What's the weather in Tehran?"}],
)
print(resp.stop_reason)   # 'tool_use'
print(resp.content)       # includes a tool_use block with input={"city": "Tehran"}

اشتباهات رایج: فراموش کردن مدیریت stop_reason: "tool_use" (اگر فقط content[0].text را چک کنید، پاسخ خالی به‌نظر می‌رسد)؛ نام‌گذاری tool با فعل‌های مبهم (run_database گنگ است، اما query_postgres_users نه).


۵.۲ تعریف tool با JSON Schema

هدف یادگیری: نوشتن JSON schema دقیق با description، enum و آرایه‌ی required تا Claude tool را درست صدا بزند.

مفاهیم کلیدی: name، description، input_schema (JSON Schema)، properties، required، enum، description برای هر property، strict: true.

تعریف یک tool یک‌سوم schema است و دو-سوم مستندسازی. فیلد description و توضیحات هر property در عمل همان prompt‌ای هستند که Claude قبل از تصمیم به فراخوانی tool می‌خواند — پس آن‌ها را مثل docstring یک function حرفه‌ای بنویسید: tool چه می‌کند، چه زمانی باید استفاده شود، هر argument چه معنا دارد، چه چیزی برمی‌گرداند. Anthropic تاکید می‌کند: وقتی توضیحات مبهم باشند، Claude یا tool اشتباه را صدا می‌زند، یا argument‌های قابل‌قبول-اما-غلط می‌سازد، یا از کاربر اطلاعاتی می‌خواهد که از قبل موجود است.

با strict: true (حالت strict)، Claude تضمین می‌کند JSON خروجی دقیقاً با schema مطابقت دارد — نه فیلد اضافی، نه فیلد required جاافتاده، نه type غلط. یک cache کامپایل grammar به‌مدت ۲۴ ساعت وجود دارد، پس هزینه‌ی first-call latency در فراخوانی‌های بعدی محو می‌شود.

نمونه کد:

search_tool = {
    "name": "search_knowledge_base",
    "description": (
        "Search the company's internal Confluence wiki for documents matching a query. "
        "Use this whenever the user asks about a topic that might be documented internally — "
        "company policies, runbooks, project plans. Returns up to 5 results with title, URL, and a 200-char excerpt."
    ),
    "input_schema": {
        "type": "object",
        "properties": {
            "query": {"type": "string", "description": "Natural-language search query in English."},
            "max_results": {"type": "integer", "minimum": 1, "maximum": 10,
                            "description": "Maximum number of results to return (default 5)."},
            "space": {"type": "string", "enum": ["engineering", "ops", "hr", "all"],
                      "description": "Confluence space to search."},
        },
        "required": ["query"],
        "additionalProperties": False,
    },
    "strict": True,
}

اشتباهات رایج: حذف description (Claude حدس می‌زند)؛ فراموش کردن additionalProperties: false (Claude فیلد اختراع می‌کند)؛ همه‌چیز را required کردن (Claude نمی‌تواند برای اطلاعات ناقص سوال بپرسد).


۵.۳ Message blocks: tool_use و tool_result

هدف یادگیری: parse کردن block‌های tool_use که Claude می‌فرستد و ساختن block‌های معتبر tool_result در پاسخ.

مفاهیم کلیدی: block از نوع tool_use (شامل id، name، input)، block از نوع tool_result (شامل tool_use_id، content، is_error)، تطابق id‌ها، چند tool_use block موازی در یک پاسخ.

پاسخ Claude یک آرایه از content block‌هاست. وقتی tool درگیر است، ممکن است ترکیبی ببینید: چند block از نوع text (Claude در حال روایت کردن کاری که قرار است انجام دهد)، یک یا چند block از نوع tool_use (هر یک با id یکتا مثل toolu_01ABC…)، و در نهایت stop_reason: "tool_use". کد شما باید همه‌ی آن‌ها را اجرا کند و در یک پیام user پاسخ دهد که content آن آرایه‌ای از block‌های tool_result است — هر کدام با tool_use_id متناظر.

is_error: true به شما اجازه می‌دهد یک فراخوانی ناموفق را علامت بزنید بدون آنکه loop شکسته شود. Claude خطا را می‌خواند و تصمیم می‌گیرد دوباره تلاش کند، tool دیگری انتخاب کند، یا از کاربر عذرخواهی کند.

نمونه کد:

def execute_tool(name, args):
    if name == "get_weather": return f"{args['city']}: 22°C, sunny"
    return f"Unknown tool {name}"

resp = client.messages.create(
    model="claude-opus-4-8", max_tokens=1024,
    tools=[weather_tool],
    messages=[{"role": "user", "content": "Weather in Tehran and Mashhad?"}],
)

if resp.stop_reason == "tool_use":
    tool_results = []
    for block in resp.content:
        if block.type == "tool_use":
            try:
                output = execute_tool(block.name, block.input)
                tool_results.append({
                    "type": "tool_result",
                    "tool_use_id": block.id,
                    "content": output,
                })
            except Exception as e:
                tool_results.append({
                    "type": "tool_result",
                    "tool_use_id": block.id,
                    "content": str(e),
                    "is_error": True,
                })
    follow_up = client.messages.create(
        model="claude-opus-4-8", max_tokens=1024,
        tools=[weather_tool],
        messages=[
            {"role": "user", "content": "Weather in Tehran and Mashhad?"},
            {"role": "assistant", "content": resp.content},   # MUST include exact assistant turn
            {"role": "user", "content": tool_results},
        ],
    )

اشتباهات رایج: عدم تطابق tool_use_id (پاسخ ۴۰۰ می‌گیرید)؛ فراموش کردن گنجاندن دقیق turn اصلی assistant در history (history باید verbatim حفظ شود)؛ برگرداندن یک dict پایتون به‌جای متن JSON-stringified در content (Claude آن را به‌عنوان متن مبهم می‌خواند — فقط string یا لیست content block پشتیبانی می‌شود).


۵.۴ Multi-turn tool use

هدف یادگیری: راه‌اندازی یک حلقه‌ی while stop_reason == "tool_use" که تا تولید پاسخ نهایی Claude ادامه دارد.

مفاهیم کلیدی: agentic loop، شرایط پایان loop (end_turn، max_tokens، stop_sequence، refusal)، انباشت history، idempotent بودن tool، سقف تعداد iteration.

شکل کانونی حلقه: تا وقتی Claude tool می‌خواهد، tool را اجرا کن. حلقه باید به سه دلیل خاتمه پیدا کند: (الف) هر stop_reason غیر از tool_use؛ (ب) یک سقف مطلق روی iteration (معمولاً ۱۰ تا ۲۰) برای جلوگیری از هزینه‌ی فرارونده؛ (ج) یک timeout برای هر فراخوانی. هر iteration دو پیام به history اضافه می‌کند: turn assistant که tool خواست، و turn کاربر که tool_result آن را برمی‌گرداند. History به‌صورت خطی رشد می‌کند — برای agent‌های طولانی، prompt caching (که در ماژول ۷ بررسی می‌شود) ضروری است.

نمونه کد:

def agent_loop(user_msg, tools, executor, max_iter=10):
    messages = [{"role": "user", "content": user_msg}]
    for _ in range(max_iter):
        resp = client.messages.create(
            model="claude-opus-4-8", max_tokens=2048, tools=tools, messages=messages,
        )
        messages.append({"role": "assistant", "content": resp.content})
        if resp.stop_reason != "tool_use":
            return resp, messages
        results = [
            {"type": "tool_result", "tool_use_id": b.id, "content": executor(b.name, b.input)}
            for b in resp.content if b.type == "tool_use"
        ]
        messages.append({"role": "user", "content": results})
    raise RuntimeError("Loop exceeded max iterations")

اشتباهات رایج: بدون iteration cap (یک باگ → bill فرارونده)؛ tool غیرidempotent (یک retry دو ایمیل می‌فرستد)؛ append کردن tool result به turn اشتباه (باید حتماً turn user باشد، نه assistant).


۵.۵ Multiple tools

هدف یادگیری: ارائه‌ی چندین tool هم‌زمان در یک request و هدایت Claude برای انتخاب (یا اجبار).

مفاهیم کلیدی: آرایه‌ی tools، پارامتر tool_choice با چهار حالت {"type": "auto"} (پیش‌فرض)، {"type": "any"}، {"type": "tool", "name": "..."}، {"type": "none"}، هدایت انتخاب tool، disable_parallel_tool_use.

به Claude می‌توان ده‌ها tool داد. پارامتر tool_choice هدایت می‌کند: auto (پیش‌فرض — Claude خود تصمیم می‌گیرد که tool صدا بزند یا نه)، any (باید یک tool صدا بزند، Claude انتخاب می‌کند کدام)، tool با نام مشخص (باید همان را صدا بزند)، یا none (ممنوع). auto در ۹۵٪ موارد گزینه‌ی درست است. any برای agent‌های ReAct-style که همیشه باید action بگیرند مناسب است؛ tool با نام مشخص برای مجبور کردن extraction به یک schema معین به‌کار می‌رود.

به‌طور پیش‌فرض Claude می‌تواند parallel tool calls بزند — چندین tool_use block در یک turn — وقتی فراخوانی‌ها مستقل از یکدیگرند. با disable_parallel_tool_use: true فراخوانی‌ها سریال می‌شوند (مفید وقتی tool‌ها side-effect دارند که باید ترتیب داشته باشند).

نمونه کد:

tools = [weather_tool, calendar_tool, email_tool]

# Auto: model decides
client.messages.create(
    model="claude-opus-4-8", max_tokens=1024, tools=tools,
    messages=[{"role": "user", "content": "What's the weather and my next meeting?"}],
)

# Force a specific tool
client.messages.create(
    model="claude-opus-4-8", max_tokens=1024, tools=tools,
    tool_choice={"type": "tool", "name": "get_weather"},
    messages=[{"role": "user", "content": "Tell me about Tehran."}],
)

# Force any tool, single call only
client.messages.create(
    model="claude-opus-4-8", max_tokens=1024, tools=tools,
    tool_choice={"type": "any", "disable_parallel_tool_use": True},
    messages=[{"role": "user", "content": "Help me plan tomorrow."}],
)

اشتباهات رایج: لیست کردن ۵۰ tool هم‌پوشان (Claude تصادفی انتخاب می‌کند — یا فضای نام‌گذاری بسازید یا از tool_search کمک بگیرید)؛ فراموش کردن یکسان نگه‌داشتن tools در همه‌ی turn‌های loop (cache می‌شکند).


۵.۶ Fine-grained tool use

هدف یادگیری: stream کردن input‌های tool همان‌طور که تولید می‌شوند، برای UX کم‌تاخیر.

مفاهیم کلیدی: event از نوع input_json_delta، انباشت partial_json، beta header از خانواده‌ی fine-grained-tool-streaming-2025-…، parser افزایشی JSON، نشانه‌گذاری UI («در حال صدا زدن search…»).

وقتی stream=True فعال باشد، Claude input‌های tool را به‌صورت رویدادهای input_json_delta می‌فرستد و شما بخش‌های partial_json را به هم می‌چسبانید. JSON کامل فقط در رویداد content_block_stop در دسترس قرار می‌گیرد. تا قبل از آن، می‌توانید UI affordance رندر کنید («در حال فراخوانی search_knowledge_base…») اما json.loads روی partial کار نمی‌کند. برخی client‌ها از یک streaming JSON parser استفاده می‌کنند تا به‌محض ظاهر شدن هر key، UI را به‌روز کنند.

نمونه کد:

buf = ""
with client.messages.stream(
    model="claude-opus-4-8", max_tokens=1024, tools=tools,
    messages=[{"role": "user", "content": "Search for Q3 OKRs"}],
) as stream:
    for event in stream:
        if event.type == "content_block_start" and event.content_block.type == "tool_use":
            print(f"[calling {event.content_block.name}]")
        elif event.type == "content_block_delta" and event.delta.type == "input_json_delta":
            buf += event.delta.partial_json
        elif event.type == "content_block_stop":
            if buf:
                import json; print("args:", json.loads(buf))
                buf = ""

اشتباهات رایج: تلاش برای parse کردن partial JSON در میانه‌ی stream (exception می‌دهد)؛ ریست نکردن buffer بین block‌ها (argumentهای خراب).


۵.۷ Text edit tool

هدف یادگیری: استفاده از schema رسمی Anthropic به نام text_editor_20250728 برای دادن قابلیت view / edit / create / insert فایل به Claude.

مفاهیم کلیدی: text_editor_20250728 (یک Anthropic-schema tool که نیاز به input_schema ندارد)، دستورات view، str_replace، create، insert، پارامتر max_characters، شماره‌ی خط ۱-indexed، view_range، backup فایل.

text edit tool یک Anthropic-schema tool است (شما input_schema تعریف نمی‌کنید — Claude قرارداد را از قبل می‌داند). Claude دستوراتی مثل {"command": "view", "path": "primes.py"} یا {"command": "str_replace", "path": ..., "old_str": ..., "new_str": ...} می‌فرستد؛ کد شما آن را روی filesystem اجرا می‌کند و نتیجه را در tool_result.content برمی‌گرداند. دستور view باید فایل را با شماره‌ی خط ۱-indexed برگرداند (مثلاً 1: def is_prime(n):) تا Claude بتواند برای insert استدلال کند.

پیاده‌سازی باید این موارد را تضمین کند: (الف) اعتبارسنجی path (ممنوعیت traversal بیرون پروژه)، (ب) بررسی unique-match برای str_replace (اگر تعداد match صفر یا بیش از یک بود، خطا برگردانید)، (ج) backup خودکار قبل از edit، (د) بررسی syntax بعد از edit (اختیاری، مثلاً ast.parse روی فایل پایتون). دستور undo_edit در نسخه‌ی text_editor_20250429 حذف شد — اگر undo می‌خواهید، آن را روی backup خود پیاده کنید.

نمونه کد:

import os, shutil

def text_editor_executor(input_):
    cmd = input_["command"]; path = input_["path"]
    if cmd == "view":
        with open(path) as f:
            return "\n".join(f"{i+1}: {l.rstrip()}" for i, l in enumerate(f))
    if cmd == "str_replace":
        with open(path) as f: text = f.read()
        n = text.count(input_["old_str"])
        if n != 1:
            return {"is_error": True, "content": f"Found {n} matches; need exactly 1."}
        shutil.copy(path, path + ".bak")
        with open(path, "w") as f:
            f.write(text.replace(input_["old_str"], input_["new_str"]))
        return "Replaced 1 occurrence."
    if cmd == "create":
        with open(path, "x") as f: f.write(input_["file_text"])
        return f"Created {path}."
    if cmd == "insert":
        with open(path) as f: lines = f.readlines()
        lines.insert(input_["insert_line"], input_["insert_text"] + "\n")
        with open(path, "w") as f: f.writelines(lines)
        return "Inserted."

tools = [{"type": "text_editor_20250728", "name": "str_replace_based_edit_tool"}]

اشتباهات رایج: برگرداندن محتوای فایل بدون شماره‌ی خط (Claude نمی‌تواند بعداً view_range بزند)؛ سکوت در برابر str_replace که چندبار match می‌شود (همیشه count بگیرید و رد کنید)؛ ماندن روی نسخه‌ی قدیمی tool (نسخه‌ی text_editor_20250124 مخصوص Sonnet 3.7 است که خودِ مدل از فوریه ۲۰۲۶ بازنشسته شده).


۵.۸ Web search tool

هدف یادگیری: فعال کردن جست‌وجوی وب server-side با web_search_20260209 (یا web_search_20250305؛ نسخه‌ی قدیمی‌تر فقط برای مدل‌های قبل از نسل 4.6) و پردازش citation‌ها.

مفاهیم کلیدی: server tool، max_uses، allowed_domains / blocked_domains، user_location، block از نوع web_search_tool_result، citation از نوع web_search_result_location، dynamic filtering، pause_turn در stop_reason، قیمت ۱۰ دلار به ازای هر هزار جست‌وجو.

web search tool یک server tool است: Anthropic داخل turn شما جست‌وجو را اجرا می‌کند. فعال‌سازی با افزودن {"type": "web_search_20260209", "name": "web_search"} به tools انجام می‌شود؛ به‌اختیار می‌توانید با max_uses، allowed_domains، blocked_domains، و user_location آن را محدود کنید. پاسخ شامل server_tool_use (query هایی که Claude زده)، web_search_tool_result (نتایج با url، title، encrypted_content)، و در نهایت block‌های text با citations از نوع web_search_result_location (هر citation یک snippet ۱۵۰ کاراکتری در cited_text دارد) است.

نسخه‌ی جدیدتر web_search_20260209 قابلیت dynamic filtering دارد: Claude با کمک code execution tool کد می‌نویسد و نتایج را قبل از ورود به context پس-پردازش می‌کند، که برای query‌های فنی مصرف token را به‌شدت پایین می‌آورد. قیمت‌گذاری: ۱۰ دلار به ازای هر هزار جست‌وجو، به‌علاوه‌ی هزینه‌ی token برای نتایجی که در context قرار می‌گیرند. شما موظفید citation‌ها را در خروجی نمایش دهید وقتی این پاسخ را به کاربر نهایی نشان می‌دهید.

نمونه کد:

resp = client.messages.create(
    model="claude-opus-4-8", max_tokens=4096,
    tools=[{
        "type": "web_search_20260209",
        "name": "web_search",
        "max_uses": 5,
        "allowed_domains": ["docs.claude.com", "anthropic.com"],
        "user_location": {"type": "approximate", "city": "Tehran",
                          "country": "IR", "timezone": "Asia/Tehran"},
    }],
    messages=[{"role": "user", "content": "Latest Claude models?"}],
)

for block in resp.content:
    if block.type == "text":
        print(block.text)
        for c in getattr(block, "citations", []) or []:
            print(f"  cite: {c.title} — {c.url}")

اشتباهات رایج: رسیدن به max_uses در میانه‌ی conversation (خطای max_uses_exceeded در نتیجه می‌آید)؛ نادیده گرفتن pause_turn (جست‌وجوهای طولانی نیاز به ادامه با re-send دارند)؛ نشان ندادن citation‌ها به کاربر نهایی (مشکل حقوقی/UX جدی).