📖

ClaudeのProgrammatic Tool Calling (PTC)はFunction Callingとは違うのか

に公開

LLM・LLM活用 Advent Calendar 2025 7日

ヤプリ&フラー 合同アドベントカレンダー Advent Calendar 2025 2日

の記事です。

概要

11月に
https://www.anthropic.com/engineering/advanced-tool-use
の発信がありました。

ここではTool Search ToolとTool Use Examplesも紹介されていますが、いろいろなところで目玉と言われているProgrammatic Tool Calling (PTC)に注目します。

2025年はAIエージェント元年と言われるだけあって、エージェントがマルチモーダルタスクを自走してこなしていくこと自体はかなりスタンダードになっています。
そして、この流れは2026年も続きそうです。

一方で、最近の各生成AIエージェントの動きとして、context windowをいかに小さく、必要十分な情報だけを持たせるのかということが一個のテーマにもなっていると思っています。
https://claude.com/blog/skills

Claudeのagent skillもその文脈に当てはまります。
codexも文字列をそのまま直接的に処理するのではなく、すぐpythonのコードを自分で書いてそのプログラムに文字列を処理させるような動きをよくします。

この、context windowを最適化するためのアプローチとして、PTCはありますが、使い方がよくわかっていませんでした。
特に、これが今までのMCPのToolやFunction callingと本質的に何が違うのかがピンときませんでした。
そこで、ポイントとなるのがOrchestrationという概念とCode Execution環境という実行環境です。これらがPTCの中でどういう意味を持つのかも理解をしていきます。

課題設定

arxiv論文のカテゴリ内訳で、近年AI agentに関する研究の割合がどのような推移を辿っているのかを調べる。

という課題を設定します。

フロー

  • arxiv論文からカテゴリがAI(ca.AI)のものを200個サンプリングする
  • この中で、特定のAI agentに関係するキーワードをabstractに含むものだけを抽出する
  • 以上のプロセスを2020年から2025年までの6年分行う。
  • 各年でAI agentに関連する論文の数の推移を棒グラフで可視化する

というフローです。
これをPTCで実現すると下記のフローになります。

コード
#!/usr/bin/env python3

import os
import json
import time
import logging
import threading
from typing import List, Dict, Any
import anthropic
import arxiv


logging.basicConfig(
    level=logging.INFO,
    format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
    handlers=[
        logging.FileHandler('arxiv_analyzer.log'),
        logging.StreamHandler()
    ]
)
logger = logging.getLogger(__name__)

def search_arxiv_papers(year: int, max_results: int = 200) -> List[Dict[str, Any]]:
    """Search arxiv papers by year and category using arxiv.query."""

    logger.info(f"Starting search for year {year} with max_results={max_results}")

    start_date = f"{year}0101"
    end_date = f"{year}1231"

    query = f'cat:cs.AI AND submittedDate:[{start_date} TO {end_date}]'
    logger.info(f"Arxiv query: {query}")

    try:
        logger.info(f"Executing arxiv.Client with max_results={max_results}...")
        client = arxiv.Client()
        search = arxiv.Search(
            query=query,
            max_results=max_results,
            sort_by=arxiv.SortCriterion.SubmittedDate
        )

        results = list(client.results(search))[:max_results]
        logger.info(f"Found {len(results)} papers for year {year}")

        papers = []
        for i, result in enumerate(results):
            papers.append({
                "title": result.title,
                "abstract": result.summary,
                "published": result.published.strftime("%Y-%m-%d"),
                "categories": result.categories
            })

        logger.info(f"Successfully processed {len(papers)} papers")
        time.sleep(1)
        return papers

    except Exception as e:
        logger.error(f"Error searching arxiv for year {year}: {str(e)}", exc_info=True)
        return []

def filter_agent_papers(papers: List[Dict[str, Any]], year: int) -> Dict[str, int]:
    """Filter and count Agent-focused papers vs Other AI based on keywords."""

    agent_keywords = ["agent", "multi-agent", "agentic", "planning", "reasoning", "tool calling", "tool use"]

    agent_count = 0
    total_count = len(papers)

    for paper in papers:
        title = paper.get("title", "").lower()
        abstract = paper.get("abstract", "").lower()
        text = title + " " + abstract

        if any(keyword in text for keyword in agent_keywords):
            agent_count += 1

    return {
        "year": year,
        "total_papers": total_count,
        "agent_papers": agent_count
    }


def process_tool_call(tool_name: str, tool_input: Dict[str, Any]) -> str:
    """Process tool calls from Claude."""

    logger.info(f"Processing tool call: {tool_name} with input: {tool_input}")

    if tool_name == "search_and_filter_papers":
        year = tool_input.get("year")
        max_results = tool_input.get("max_results", 200)

        logger.info(f"Tool parameters: year={year}, max_results={max_results}")

        # Search papers
        papers = search_arxiv_papers(year, max_results)
        logger.info(f"Retrieved {len(papers)} papers for {year}")

        # Filter papers and return count summary
        filter_results = filter_agent_papers(papers, year)

        result = json.dumps(filter_results, ensure_ascii=False)
        other_count = filter_results['total_papers'] - filter_results['agent_papers']
        logger.info(f"Filter result: {filter_results['agent_papers']} Agent, {other_count} Other out of {filter_results['total_papers']} total")
        return result

    logger.warning(f"Unknown tool: {tool_name}")
    return f"Unknown tool: {tool_name}"

def run_analysis() -> str:
    """Main analysis function using advanced PTC with Claude. Returns markdown report."""

    logger.info("Starting arxiv trend analysis")

    api_key = os.getenv("ANTHROPIC_API_KEY")
    if not api_key:
        logger.error("ANTHROPIC_API_KEY environment variable not set")
        raise ValueError("ANTHROPIC_API_KEY environment variable not set")

    logger.info("Using direct Anthropic API")
    client = anthropic.Anthropic(api_key=api_key)

    tools = [
        {
            "type": "code_execution_20250825",
            "name": "code_execution"
        },
        {
            "name": "search_and_filter_papers",
            "description": "Search arxiv papers and filter for Agent-related papers (returns only count summary)",
            "input_schema": {
                "type": "object",
                "properties": {
                    "year": {
                        "type": "integer",
                        "description": "Year to search (2020-2025)"
                    },
                    "max_results": {
                        "type": "integer",
                        "description": "Maximum papers to return",
                        "default": 200
                    }
                },
                "required": ["year"]
            }
        }
    ]

    initial_prompt = """タスク: cs.AI論文のAgent研究増加傾向分析(2020-2025)

目的: cs.AIカテゴリの論文(2020-2025年、各年200件)のタイトル・アブストラクトを分析し、Agent研究の増加傾向を示す

実行手順:
1. 2020年から2025年まで各年でsearch_and_filter_papersツールを呼び出し:
   - 各年のcs.AI論文を200件検索
   - Agent関連キーワードでフィルタリング実行
   - 集計結果を受け取り: {"year": 2020, "total_papers": 200, "agent_papers": 10}

2. code_executionで受け取った各年の結果からASCII棒グラフ生成:
   - numpy.histogramを使用してヒストグラムデータを作成
   - 年別Agent論文数と比率を計算
   - ASCII文字(█)を使って6年間の棒グラフを描画

最終出力形式(この形式のみ):

2020年 : ███ 15論文 (7.50%)
2021年 : ████ 20論文 (10.00%)
2022年 : ██████ 30論文 (15.00%)
2023年 : ████████ 40論文 (20.00%)
2024年 : ██████████ 50論文 (25.00%)
2025年 : ████████████ 60論文 (30.00%)


重要:
- search_and_filter_papersツールを2020年から2025年で各1回ずつ(計6回)呼び出し
- code_executionでnumpyを活用:
  * import numpy as np
  * 各年のAgent論文数データを配列として作成
  * numpy.histogram()を使ってヒストグラムを計算
  * ヒストグラムの結果を基にASCII棒の長さを決定
- 受け取った結果でASCII棒グラフを作成
- 上記の形式のみを出力(他の説明や分析は不要)
- ASCII棒グラフは```コードブロックで囲む"""

    messages = [{"role": "user", "content": initial_prompt}]

    logger.info("Starting conversation with Claude")
    logger.info("=== INITIAL PROMPT ===")
    logger.info(initial_prompt)
    logger.info("========================")
    turn_count = 0
    markdown_result = ""

    while True:
        turn_count += 1
        logger.info(f"Turn {turn_count}: Sending request to Claude")

        # Log input message size and details
        total_chars = sum(len(str(msg)) for msg in messages)
        logger.info(f"=== INPUT SIZE (Turn {turn_count}) ===")
        logger.info(f"Messages count: {len(messages)}")
        logger.info(f"Total characters: {total_chars}")
        logger.info(f"Estimated input tokens: ~{total_chars // 4}")

        # Show recent message roles for debugging
        logger.info(f"Message roles: {[msg['role'] for msg in messages[-5:]]}")  # Last 5 messages
        logger.info(f"=====================================")

        try:
            # Use non-streaming for proper PTC behavior
            logger.info(f"Starting PTC request...")
            start_time = time.time()

            # Progress indicator for long requests
            def progress_indicator():
                dots = 0
                while not hasattr(progress_indicator, 'stop'):
                    elapsed = time.time() - start_time
                    print(f"\r[{elapsed:.1f}s] Processing PTC{'.' * (dots % 4)}", end='', flush=True)
                    dots += 1
                    time.sleep(1)

            progress_thread = threading.Thread(target=progress_indicator)
            progress_thread.daemon = True
            progress_thread.start()

            # Use non-streaming API with extended timeout for PTC
            response = client.beta.messages.create(
                betas=["advanced-tool-use-2025-11-20"],
                model="claude-sonnet-4-5-20250929",
                max_tokens=10000,
                tools=tools,
                messages=messages,
                timeout=300  # 5 minutes timeout
            )

            # Stop progress indicator
            progress_indicator.stop = True
            elapsed_total = time.time() - start_time
            logger.info(f"PTC request completed in {elapsed_total:.2f} seconds")

            if not response:
                logger.error("No response received")
                break

            # Check if response has expected attributes
            if not hasattr(response, 'stop_reason'):
                logger.error(f"Response object type: {type(response)}")
                logger.error(f"Response attributes: {dir(response)}")
                break

            logger.info(f"Received response with stop_reason: {response.stop_reason}")

            # Print token usage
            if hasattr(response, 'usage'):
                logger.info(f"=== TOKEN USAGE (Turn {turn_count}) ===")
                logger.info(f"Input tokens: {response.usage.input_tokens}")
                logger.info(f"Output tokens: {response.usage.output_tokens}")
                logger.info(f"Total tokens: {response.usage.input_tokens + response.usage.output_tokens}")
                logger.info(f"=====================================")

            # Log response metadata
            logger.info(f"=== RESPONSE METADATA ===")
            logger.info(f"Model: {response.model}")
            logger.info(f"Stop reason: {response.stop_reason}")
            if hasattr(response, 'id'):
                logger.info(f"Response ID: {response.id}")
            logger.info(f"Content blocks: {len(response.content)}")
            logger.info(f"========================")

            logger.info(f"=== CLAUDE RESPONSE (Turn {turn_count}) ===")
            for i, block in enumerate(response.content):
                logger.info(f"Block {i} ({block.type}):")
                if block.type == "text":
                    logger.info(f"Text: {block.text[:200]}{'...' if len(block.text) > 200 else ''}")
                    # Collect only the final markdown result with ASCII bar chart
                    if "```" in block.text and ("年" in block.text and "論文" in block.text):
                        markdown_result = block.text  # Only keep the final result
                elif block.type == "tool_use":
                    logger.info(f"Tool: {block.name}")
                    logger.info(f"Input: {block.input}")
                    logger.info(f"ID: {block.id}")
                elif block.type == "server_tool_use":
                    logger.info(f"Server Tool: {block.name}")
                    logger.info(f"ID: {block.id}")
                    if hasattr(block, 'input'):
                        logger.info(f"Input: {block.input}")
                        # Log Python code if it's code execution
                        if block.name in ['text_editor_code_execution', 'bash_code_execution']:
                            if 'file_text' in block.input:
                                logger.info(f"Generated Python Code:")
                                logger.info(block.input['file_text'])
                            elif 'command' in block.input:
                                logger.info(f"Bash Command: {block.input['command']}")
                    # Log full content for code generation verification
                    logger.info("Full server tool content:")
                    for attr in ['input', 'name', 'id', 'type']:
                        if hasattr(block, attr):
                            value = getattr(block, attr)
                            logger.info(f"  {attr}: {value}")
                elif hasattr(block, 'type') and 'tool_result' in block.type:
                    logger.info(f"Tool Result Type: {block.type}")
                    if hasattr(block, 'tool_use_id'):
                        logger.info(f"Tool Use ID: {block.tool_use_id}")
                    if hasattr(block, 'content'):
                        content = str(block.content)[:500]  # 増やして詳細表示
                        logger.info(f"Content: {content}{'...' if len(str(block.content)) > 500 else ''}")
                        # Log stdout if available (for bash execution results)
                        if hasattr(block, 'content') and hasattr(block.content, 'stdout'):
                            logger.info(f"Stdout: {block.content.stdout}")
                else:
                    logger.info(f"Unknown block type: {block.type}")
                    # Log all available attributes for unknown types
                    logger.info("All attributes:")
                    for attr in dir(block):
                        if not attr.startswith('_'):
                            try:
                                value = getattr(block, attr)
                                if not callable(value):
                                    logger.info(f"  {attr}: {value}")
                            except:
                                pass
                logger.info("---")
            logger.info(f"==========================================")

            if response.stop_reason == "end_turn":
                logger.info("Conversation completed")
                break

            if response.stop_reason == "tool_use":
                logger.info("Processing tool uses")
                tool_results = []

                for i, block in enumerate(response.content):
                    logger.debug(f"Processing content block {i}: type={block.type}")

                    if block.type == "tool_use":
                        logger.info(f"Executing tool: {block.name}")
                        result = process_tool_call(block.name, block.input)
                        tool_results.append({
                            "type": "tool_result",
                            "tool_use_id": block.id,
                            "content": result
                        })
                        logger.info(f"Tool {block.name} execution completed")

                    elif block.type == "server_tool_use":
                        logger.info(f"Server tool executed: {block.name}")
                        # Server tool results are handled automatically, don't add to tool_results

                messages.append({"role": "assistant", "content": response.content})
                if tool_results:  # Only add if there are actual tool results
                    messages.append({"role": "user", "content": tool_results})

                logger.info(f"Added {len(tool_results)} tool results to conversation")
                logger.info(f"Total conversation length: {len(messages)} messages")


        except Exception as e:
            logger.error(f"Error during API call on turn {turn_count}: {str(e)}", exc_info=True)
            logger.error("Stopping due to repeated API errors")
            break

    # Analysis completed - return markdown result
    logger.info("Analysis completed successfully")
    return markdown_result.strip() if markdown_result else "# Analysis completed but no markdown result found"


def main():
    """Entry point for the application."""
    logger.info("Application started")

    try:
        markdown_result = run_analysis()

        # Save markdown result to output.md
        output_path = "output.md"
        with open(output_path, "w", encoding="utf-8") as f:
            f.write(markdown_result)

        logger.info(f"Analysis completed and saved to {output_path}")
        logger.info(f"Saved {len(markdown_result)} characters")

    except KeyboardInterrupt:
        logger.info("Application interrupted by user")
    except Exception as e:
        logger.error(f"Application failed: {str(e)}", exc_info=True)
        raise


if __name__ == "__main__":
    main()

結果

AI agentに関係する論文数の推移(サンプリングした100論文に対する割合)

2020年 : ████████ 44論文 (22.00%)
2021年 : ███████ 36論文 (18.00%)
2022年 : ████████ 42論文 (21.00%)
2023年 : ████████ 44論文 (22.00%)
2024年 : █████████ 48論文 (24.00%)
2025年 : ████████████████ 80論文 (40.00%)

ASCII棒グラフを作ってくれました。
AI agentに関係する論文が2025年から大きく増えていることが確認できました。
抽出方法は上述の通りある程度ラフにやっており、結果についての有意性の検定もしていないので結果自体は参考までにお願いします。

ログ全文
2025-12-03 01:03:24,128 - __main__ - INFO - Application started
2025-12-03 01:03:24,128 - __main__ - INFO - Starting arxiv trend analysis
2025-12-03 01:03:24,128 - __main__ - INFO - Using direct Anthropic API
2025-12-03 01:03:24,138 - __main__ - INFO - Starting conversation with Claude
2025-12-03 01:03:24,138 - __main__ - INFO - === INITIAL PROMPT ===
2025-12-03 01:03:24,138 - __main__ - INFO - タスク: cs.AI論文のAgent研究増加傾向分析(2020-2025)

目的: cs.AIカテゴリの論文(2020-2025年、各年200件)のタイトル・アブストラクトを分析し、Agent研究の増加傾向を示す

実行手順:
1. 2020年から2025年まで各年でsearch_and_filter_papersツールを呼び出し:
   - 各年のcs.AI論文を200件検索
   - Agent関連キーワードでフィルタリング実行
   - 集計結果を受け取り: {"year": 2020, "total_papers": 200, "agent_papers": 10}

2. code_executionで受け取った各年の結果からASCII棒グラフ生成:
   - numpy.histogramを使用してヒストグラムデータを作成
   - 年別Agent論文数と比率を計算
   - ASCII文字(█)を使って6年間の棒グラフを描画

最終出力形式(この形式のみ):
2020: ███ 15論文 (7.50%)
2021: ████ 20論文 (10.00%)
2022: ██████ 30論文 (15.00%)
2023: ████████ 40論文 (20.00%)
2024: ██████████ 50論文 (25.00%)
2025: ████████████ 60論文 (30.00%)

重要:
- search_and_filter_papersツールを2020年から2025年で各1回ずつ(計6回)呼び出し
- code_executionでnumpyを活用:
  * import numpy as np
  * 各年のAgent論文数データを配列として作成
  * numpy.histogram()を使ってヒストグラムを計算
  * ヒストグラムの結果を基にASCII棒の長さを決定
- 受け取った結果でASCII棒グラフを作成
- 上記の形式のみを出力(他の説明や分析は不要)
- ASCII棒グラフは```コードブロックで囲む
2025-12-03 01:03:24,138 - __main__ - INFO - ========================
2025-12-03 01:03:24,138 - __main__ - INFO - Turn 1: Sending request to Claude
2025-12-03 01:03:24,138 - __main__ - INFO - === INPUT SIZE (Turn 1) ===
2025-12-03 01:03:24,138 - __main__ - INFO - Messages count: 1
2025-12-03 01:03:24,138 - __main__ - INFO - Total characters: 974
2025-12-03 01:03:24,138 - __main__ - INFO - Estimated input tokens: ~243
2025-12-03 01:03:24,138 - __main__ - INFO - Message roles: ['user']
2025-12-03 01:03:24,138 - __main__ - INFO - =====================================
2025-12-03 01:03:24,138 - __main__ - INFO - Starting PTC request...
2025-12-03 01:03:30,506 - httpx - INFO - HTTP Request: POST https://api.anthropic.com/v1/messages?beta=true "HTTP/1.1 200 OK"
2025-12-03 01:03:30,535 - __main__ - INFO - PTC request completed in 6.40 seconds
2025-12-03 01:03:30,535 - __main__ - INFO - Received response with stop_reason: tool_use
2025-12-03 01:03:30,535 - __main__ - INFO - === TOKEN USAGE (Turn 1) ===
2025-12-03 01:03:30,535 - __main__ - INFO - Input tokens: 2974
2025-12-03 01:03:30,535 - __main__ - INFO - Output tokens: 434
2025-12-03 01:03:30,535 - __main__ - INFO - Total tokens: 3408
2025-12-03 01:03:30,535 - __main__ - INFO - =====================================
2025-12-03 01:03:30,535 - __main__ - INFO - === RESPONSE METADATA ===
2025-12-03 01:03:30,535 - __main__ - INFO - Model: claude-sonnet-4-5-20250929
2025-12-03 01:03:30,535 - __main__ - INFO - Stop reason: tool_use
2025-12-03 01:03:30,536 - __main__ - INFO - Response ID: msg_01Nvwb3wznP8nP3Ty5UBaK3t
2025-12-03 01:03:30,536 - __main__ - INFO - Content blocks: 7
2025-12-03 01:03:30,536 - __main__ - INFO - ========================
2025-12-03 01:03:30,536 - __main__ - INFO - === CLAUDE RESPONSE (Turn 1) ===
2025-12-03 01:03:30,536 - __main__ - INFO - Block 0 (text):
2025-12-03 01:03:30,536 - __main__ - INFO - Text: 了解しました。2020年から2025年までの各年でcs.AI論文を検索し、Agent研究の増加傾向を分析します。

まず、各年のデータを収集します。
2025-12-03 01:03:30,536 - __main__ - INFO - ---
2025-12-03 01:03:30,536 - __main__ - INFO - Block 1 (tool_use):
2025-12-03 01:03:30,536 - __main__ - INFO - Tool: search_and_filter_papers
2025-12-03 01:03:30,536 - __main__ - INFO - Input: {'year': 2020, 'max_results': 200}
2025-12-03 01:03:30,536 - __main__ - INFO - ID: toolu_011p94C8nhqRCPLJVcBirJpn
2025-12-03 01:03:30,536 - __main__ - INFO - ---
2025-12-03 01:03:30,536 - __main__ - INFO - Block 2 (tool_use):
2025-12-03 01:03:30,536 - __main__ - INFO - Tool: search_and_filter_papers
2025-12-03 01:03:30,536 - __main__ - INFO - Input: {'year': 2021, 'max_results': 200}
2025-12-03 01:03:30,536 - __main__ - INFO - ID: toolu_017NFSTj68Ki5MysDVrurkUt
2025-12-03 01:03:30,536 - __main__ - INFO - ---
2025-12-03 01:03:30,536 - __main__ - INFO - Block 3 (tool_use):
2025-12-03 01:03:30,536 - __main__ - INFO - Tool: search_and_filter_papers
2025-12-03 01:03:30,536 - __main__ - INFO - Input: {'year': 2022, 'max_results': 200}
2025-12-03 01:03:30,536 - __main__ - INFO - ID: toolu_011MmsU5jtQmcBvDCQFGqWq6
2025-12-03 01:03:30,536 - __main__ - INFO - ---
2025-12-03 01:03:30,536 - __main__ - INFO - Block 4 (tool_use):
2025-12-03 01:03:30,536 - __main__ - INFO - Tool: search_and_filter_papers
2025-12-03 01:03:30,536 - __main__ - INFO - Input: {'year': 2023, 'max_results': 200}
2025-12-03 01:03:30,536 - __main__ - INFO - ID: toolu_011eZmw82uWd3o6NbuE211fF
2025-12-03 01:03:30,536 - __main__ - INFO - ---
2025-12-03 01:03:30,536 - __main__ - INFO - Block 5 (tool_use):
2025-12-03 01:03:30,536 - __main__ - INFO - Tool: search_and_filter_papers
2025-12-03 01:03:30,536 - __main__ - INFO - Input: {'year': 2024, 'max_results': 200}
2025-12-03 01:03:30,536 - __main__ - INFO - ID: toolu_01GyTPeeh8rzks1qPsJPU3dm
2025-12-03 01:03:30,536 - __main__ - INFO - ---
2025-12-03 01:03:30,536 - __main__ - INFO - Block 6 (tool_use):
2025-12-03 01:03:30,536 - __main__ - INFO - Tool: search_and_filter_papers
2025-12-03 01:03:30,536 - __main__ - INFO - Input: {'year': 2025, 'max_results': 200}
2025-12-03 01:03:30,537 - __main__ - INFO - ID: toolu_01GS4LZpPYrm23TyD3uBJyAU
2025-12-03 01:03:30,537 - __main__ - INFO - ---
2025-12-03 01:03:30,537 - __main__ - INFO - ==========================================
2025-12-03 01:03:30,537 - __main__ - INFO - Processing tool uses
2025-12-03 01:03:30,537 - __main__ - INFO - Executing tool: search_and_filter_papers
2025-12-03 01:03:30,537 - __main__ - INFO - Processing tool call: search_and_filter_papers with input: {'year': 2020, 'max_results': 200}
2025-12-03 01:03:30,537 - __main__ - INFO - Tool parameters: year=2020, max_results=200
2025-12-03 01:03:30,537 - __main__ - INFO - Starting search for year 2020 with max_results=200
2025-12-03 01:03:30,537 - __main__ - INFO - Arxiv query: cat:cs.AI AND submittedDate:[20200101 TO 20201231]
2025-12-03 01:03:30,537 - __main__ - INFO - Executing arxiv.Client with max_results=200...
2025-12-03 01:03:30,537 - arxiv - INFO - Requesting page (first: True, try: 0): https://export.arxiv.org/api/query?search_query=cat%3Acs.AI+AND+submittedDate%3A%5B20200101+TO+20201231%5D&id_list=&sortBy=submittedDate&sortOrder=descending&start=0&max_results=100
2025-12-03 01:03:31,095 - arxiv - INFO - Got first page: 100 of 7415 total results
2025-12-03 01:03:31,098 - arxiv - INFO - Sleeping: 2.944307 seconds
2025-12-03 01:03:34,042 - arxiv - INFO - Requesting page (first: False, try: 0): https://export.arxiv.org/api/query?search_query=cat%3Acs.AI+AND+submittedDate%3A%5B20200101+TO+20201231%5D&id_list=&sortBy=submittedDate&sortOrder=descending&start=100&max_results=100
2025-12-03 01:03:34,586 - __main__ - INFO - Found 200 papers for year 2020
2025-12-03 01:03:34,586 - __main__ - INFO - Successfully processed 200 papers
2025-12-03 01:03:35,597 - __main__ - INFO - Retrieved 200 papers for 2020
2025-12-03 01:03:35,598 - __main__ - INFO - Filter result: 44 Agent, 156 Other out of 200 total
2025-12-03 01:03:35,598 - __main__ - INFO - Tool search_and_filter_papers execution completed
2025-12-03 01:03:35,599 - __main__ - INFO - Executing tool: search_and_filter_papers
2025-12-03 01:03:35,599 - __main__ - INFO - Processing tool call: search_and_filter_papers with input: {'year': 2021, 'max_results': 200}
2025-12-03 01:03:35,599 - __main__ - INFO - Tool parameters: year=2021, max_results=200
2025-12-03 01:03:35,599 - __main__ - INFO - Starting search for year 2021 with max_results=200
2025-12-03 01:03:35,599 - __main__ - INFO - Arxiv query: cat:cs.AI AND submittedDate:[20210101 TO 20211231]
2025-12-03 01:03:35,599 - __main__ - INFO - Executing arxiv.Client with max_results=200...
2025-12-03 01:03:35,600 - arxiv - INFO - Requesting page (first: True, try: 0): https://export.arxiv.org/api/query?search_query=cat%3Acs.AI+AND+submittedDate%3A%5B20210101+TO+20211231%5D&id_list=&sortBy=submittedDate&sortOrder=descending&start=0&max_results=100
2025-12-03 01:03:36,332 - arxiv - INFO - Got first page: 100 of 12519 total results
2025-12-03 01:03:36,336 - arxiv - INFO - Sleeping: 2.927958 seconds
2025-12-03 01:03:39,269 - arxiv - INFO - Requesting page (first: False, try: 0): https://export.arxiv.org/api/query?search_query=cat%3Acs.AI+AND+submittedDate%3A%5B20210101+TO+20211231%5D&id_list=&sortBy=submittedDate&sortOrder=descending&start=100&max_results=100
2025-12-03 01:03:39,914 - __main__ - INFO - Found 200 papers for year 2021
2025-12-03 01:03:39,915 - __main__ - INFO - Successfully processed 200 papers
2025-12-03 01:03:40,922 - __main__ - INFO - Retrieved 200 papers for 2021
2025-12-03 01:03:40,924 - __main__ - INFO - Filter result: 36 Agent, 164 Other out of 200 total
2025-12-03 01:03:40,924 - __main__ - INFO - Tool search_and_filter_papers execution completed
2025-12-03 01:03:40,924 - __main__ - INFO - Executing tool: search_and_filter_papers
2025-12-03 01:03:40,924 - __main__ - INFO - Processing tool call: search_and_filter_papers with input: {'year': 2022, 'max_results': 200}
2025-12-03 01:03:40,924 - __main__ - INFO - Tool parameters: year=2022, max_results=200
2025-12-03 01:03:40,924 - __main__ - INFO - Starting search for year 2022 with max_results=200
2025-12-03 01:03:40,924 - __main__ - INFO - Arxiv query: cat:cs.AI AND submittedDate:[20220101 TO 20221231]
2025-12-03 01:03:40,924 - __main__ - INFO - Executing arxiv.Client with max_results=200...
2025-12-03 01:03:40,924 - arxiv - INFO - Requesting page (first: True, try: 0): https://export.arxiv.org/api/query?search_query=cat%3Acs.AI+AND+submittedDate%3A%5B20220101+TO+20221231%5D&id_list=&sortBy=submittedDate&sortOrder=descending&start=0&max_results=100
2025-12-03 01:03:41,565 - arxiv - INFO - Got first page: 100 of 14804 total results
2025-12-03 01:03:41,568 - arxiv - INFO - Sleeping: 2.929425 seconds
2025-12-03 01:03:44,503 - arxiv - INFO - Requesting page (first: False, try: 0): https://export.arxiv.org/api/query?search_query=cat%3Acs.AI+AND+submittedDate%3A%5B20220101+TO+20221231%5D&id_list=&sortBy=submittedDate&sortOrder=descending&start=100&max_results=100
2025-12-03 01:03:45,016 - __main__ - INFO - Found 200 papers for year 2022
2025-12-03 01:03:45,017 - __main__ - INFO - Successfully processed 200 papers
2025-12-03 01:03:46,028 - __main__ - INFO - Retrieved 200 papers for 2022
2025-12-03 01:03:46,030 - __main__ - INFO - Filter result: 42 Agent, 158 Other out of 200 total
2025-12-03 01:03:46,031 - __main__ - INFO - Tool search_and_filter_papers execution completed
2025-12-03 01:03:46,031 - __main__ - INFO - Executing tool: search_and_filter_papers
2025-12-03 01:03:46,031 - __main__ - INFO - Processing tool call: search_and_filter_papers with input: {'year': 2023, 'max_results': 200}
2025-12-03 01:03:46,031 - __main__ - INFO - Tool parameters: year=2023, max_results=200
2025-12-03 01:03:46,031 - __main__ - INFO - Starting search for year 2023 with max_results=200
2025-12-03 01:03:46,031 - __main__ - INFO - Arxiv query: cat:cs.AI AND submittedDate:[20230101 TO 20231231]
2025-12-03 01:03:46,031 - __main__ - INFO - Executing arxiv.Client with max_results=200...
2025-12-03 01:03:46,031 - arxiv - INFO - Requesting page (first: True, try: 0): https://export.arxiv.org/api/query?search_query=cat%3Acs.AI+AND+submittedDate%3A%5B20230101+TO+20231231%5D&id_list=&sortBy=submittedDate&sortOrder=descending&start=0&max_results=100
2025-12-03 01:03:46,628 - arxiv - INFO - Got first page: 100 of 21847 total results
2025-12-03 01:03:46,631 - arxiv - INFO - Sleeping: 2.924892 seconds
2025-12-03 01:03:49,560 - arxiv - INFO - Requesting page (first: False, try: 0): https://export.arxiv.org/api/query?search_query=cat%3Acs.AI+AND+submittedDate%3A%5B20230101+TO+20231231%5D&id_list=&sortBy=submittedDate&sortOrder=descending&start=100&max_results=100
2025-12-03 01:03:50,075 - __main__ - INFO - Found 200 papers for year 2023
2025-12-03 01:03:50,076 - __main__ - INFO - Successfully processed 200 papers
2025-12-03 01:03:51,082 - __main__ - INFO - Retrieved 200 papers for 2023
2025-12-03 01:03:51,083 - __main__ - INFO - Filter result: 44 Agent, 156 Other out of 200 total
2025-12-03 01:03:51,083 - __main__ - INFO - Tool search_and_filter_papers execution completed
2025-12-03 01:03:51,083 - __main__ - INFO - Executing tool: search_and_filter_papers
2025-12-03 01:03:51,083 - __main__ - INFO - Processing tool call: search_and_filter_papers with input: {'year': 2024, 'max_results': 200}
2025-12-03 01:03:51,083 - __main__ - INFO - Tool parameters: year=2024, max_results=200
2025-12-03 01:03:51,083 - __main__ - INFO - Starting search for year 2024 with max_results=200
2025-12-03 01:03:51,083 - __main__ - INFO - Arxiv query: cat:cs.AI AND submittedDate:[20240101 TO 20241231]
2025-12-03 01:03:51,083 - __main__ - INFO - Executing arxiv.Client with max_results=200...
2025-12-03 01:03:51,083 - arxiv - INFO - Requesting page (first: True, try: 0): https://export.arxiv.org/api/query?search_query=cat%3Acs.AI+AND+submittedDate%3A%5B20240101+TO+20241231%5D&id_list=&sortBy=submittedDate&sortOrder=descending&start=0&max_results=100
2025-12-03 01:03:51,728 - arxiv - INFO - Got first page: 100 of 33021 total results
2025-12-03 01:03:51,731 - arxiv - INFO - Sleeping: 2.910398 seconds
2025-12-03 01:03:54,647 - arxiv - INFO - Requesting page (first: False, try: 0): https://export.arxiv.org/api/query?search_query=cat%3Acs.AI+AND+submittedDate%3A%5B20240101+TO+20241231%5D&id_list=&sortBy=submittedDate&sortOrder=descending&start=100&max_results=100
2025-12-03 01:03:55,269 - __main__ - INFO - Found 200 papers for year 2024
2025-12-03 01:03:55,270 - __main__ - INFO - Successfully processed 200 papers
2025-12-03 01:03:56,281 - __main__ - INFO - Retrieved 200 papers for 2024
2025-12-03 01:03:56,283 - __main__ - INFO - Filter result: 48 Agent, 152 Other out of 200 total
2025-12-03 01:03:56,283 - __main__ - INFO - Tool search_and_filter_papers execution completed
2025-12-03 01:03:56,283 - __main__ - INFO - Executing tool: search_and_filter_papers
2025-12-03 01:03:56,283 - __main__ - INFO - Processing tool call: search_and_filter_papers with input: {'year': 2025, 'max_results': 200}
2025-12-03 01:03:56,283 - __main__ - INFO - Tool parameters: year=2025, max_results=200
2025-12-03 01:03:56,283 - __main__ - INFO - Starting search for year 2025 with max_results=200
2025-12-03 01:03:56,283 - __main__ - INFO - Arxiv query: cat:cs.AI AND submittedDate:[20250101 TO 20251231]
2025-12-03 01:03:56,283 - __main__ - INFO - Executing arxiv.Client with max_results=200...
2025-12-03 01:03:56,284 - arxiv - INFO - Requesting page (first: True, try: 0): https://export.arxiv.org/api/query?search_query=cat%3Acs.AI+AND+submittedDate%3A%5B20250101+TO+20251231%5D&id_list=&sortBy=submittedDate&sortOrder=descending&start=0&max_results=100
2025-12-03 01:03:56,922 - arxiv - INFO - Got first page: 100 of 41634 total results
2025-12-03 01:03:56,926 - arxiv - INFO - Sleeping: 2.918240 seconds
2025-12-03 01:03:59,848 - arxiv - INFO - Requesting page (first: False, try: 0): https://export.arxiv.org/api/query?search_query=cat%3Acs.AI+AND+submittedDate%3A%5B20250101+TO+20251231%5D&id_list=&sortBy=submittedDate&sortOrder=descending&start=100&max_results=100
2025-12-03 01:04:00,597 - __main__ - INFO - Found 200 papers for year 2025
2025-12-03 01:04:00,597 - __main__ - INFO - Successfully processed 200 papers
2025-12-03 01:04:01,608 - __main__ - INFO - Retrieved 200 papers for 2025
2025-12-03 01:04:01,612 - __main__ - INFO - Filter result: 80 Agent, 120 Other out of 200 total
2025-12-03 01:04:01,612 - __main__ - INFO - Tool search_and_filter_papers execution completed
2025-12-03 01:04:01,612 - __main__ - INFO - Added 6 tool results to conversation
2025-12-03 01:04:01,612 - __main__ - INFO - Total conversation length: 3 messages
2025-12-03 01:04:01,612 - __main__ - INFO - Turn 2: Sending request to Claude
2025-12-03 01:04:01,613 - __main__ - INFO - === INPUT SIZE (Turn 2) ===
2025-12-03 01:04:01,613 - __main__ - INFO - Messages count: 3
2025-12-03 01:04:01,613 - __main__ - INFO - Total characters: 3074
2025-12-03 01:04:01,613 - __main__ - INFO - Estimated input tokens: ~768
2025-12-03 01:04:01,613 - __main__ - INFO - Message roles: ['user', 'assistant', 'user']
2025-12-03 01:04:01,613 - __main__ - INFO - =====================================
2025-12-03 01:04:01,613 - __main__ - INFO - Starting PTC request...
2025-12-03 01:04:28,728 - httpx - INFO - HTTP Request: POST https://api.anthropic.com/v1/messages?beta=true "HTTP/1.1 200 OK"
2025-12-03 01:04:28,757 - __main__ - INFO - PTC request completed in 27.14 seconds
2025-12-03 01:04:28,758 - __main__ - INFO - Received response with stop_reason: end_turn
2025-12-03 01:04:28,758 - __main__ - INFO - === TOKEN USAGE (Turn 2) ===
2025-12-03 01:04:28,758 - __main__ - INFO - Input tokens: 12735
2025-12-03 01:04:28,758 - __main__ - INFO - Output tokens: 854
2025-12-03 01:04:28,758 - __main__ - INFO - Total tokens: 13589
2025-12-03 01:04:28,758 - __main__ - INFO - =====================================
2025-12-03 01:04:28,758 - __main__ - INFO - === RESPONSE METADATA ===
2025-12-03 01:04:28,758 - __main__ - INFO - Model: claude-sonnet-4-5-20250929
2025-12-03 01:04:28,758 - __main__ - INFO - Stop reason: end_turn
2025-12-03 01:04:28,758 - __main__ - INFO - Response ID: msg_01KY5LFmeMwH2xebGqBCCNHn
2025-12-03 01:04:28,758 - __main__ - INFO - Content blocks: 6
2025-12-03 01:04:28,758 - __main__ - INFO - ========================
2025-12-03 01:04:28,758 - __main__ - INFO - === CLAUDE RESPONSE (Turn 2) ===
2025-12-03 01:04:28,758 - __main__ - INFO - Block 0 (text):
2025-12-03 01:04:28,758 - __main__ - INFO - Text: 次に、収集したデータを使ってASCII棒グラフを生成します。
2025-12-03 01:04:28,758 - __main__ - INFO - ---
2025-12-03 01:04:28,758 - __main__ - INFO - Block 1 (server_tool_use):
2025-12-03 01:04:28,758 - __main__ - INFO - Server Tool: text_editor_code_execution
2025-12-03 01:04:28,758 - __main__ - INFO - ID: srvtoolu_01KHi1FMMLByoS8SmaHyZ6U2
2025-12-03 01:04:28,759 - __main__ - INFO - Input: {'command': 'create', 'path': '/tmp/agent_trend_analysis.py', 'file_text': 'import numpy as np\n\n# 各年のデータ\ndata = [\n    {"year": 2020, "total_papers": 200, "agent_papers": 44},\n    {"year": 2021, "total_papers": 200, "agent_papers": 36},\n    {"year": 2022, "total_papers": 200, "agent_papers": 42},\n    {"year": 2023, "total_papers": 200, "agent_papers": 44},\n    {"year": 2024, "total_papers": 200, "agent_papers": 48},\n    {"year": 2025, "total_papers": 200, "agent_papers": 80},\n]\n\n# データを配列として準備\nyears = np.array([d["year"] for d in data])\nagent_counts = np.array([d["agent_papers"] for d in data])\ntotal_counts = np.array([d["total_papers"] for d in data])\n\n# ヒストグラムの計算(numpy.histogramを使用)\n# binsを年の境界で設定\nbins = np.arange(2020, 2027)\nhist, bin_edges = np.histogram(years, bins=bins, weights=agent_counts)\n\n# 棒グラフの最大長を決定(最大値に対して)\nmax_count = np.max(agent_counts)\nmax_bar_length = 20  # 最大棒の長さ\n\n# ASCII棒グラフを出力\nprint("```")\nfor i, d in enumerate(data):\n    year = d["year"]\n    agent_papers = d["agent_papers"]\n    total_papers = d["total_papers"]\n    percentage = (agent_papers / total_papers) * 100\n    \n    # 棒の長さを計算(numpy使用)\n    bar_length = int(np.ceil(agent_papers / max_count * max_bar_length))\n    bar = "█" * bar_length\n    \n    print(f"{year}年 : {bar} {agent_papers}論文 ({percentage:.2f}%)")\nprint("```")\n'}
2025-12-03 01:04:28,759 - __main__ - INFO - Generated Python Code:
2025-12-03 01:04:28,759 - __main__ - INFO - import numpy as np

# 各年のデータ
data = [
    {"year": 2020, "total_papers": 200, "agent_papers": 44},
    {"year": 2021, "total_papers": 200, "agent_papers": 36},
    {"year": 2022, "total_papers": 200, "agent_papers": 42},
    {"year": 2023, "total_papers": 200, "agent_papers": 44},
    {"year": 2024, "total_papers": 200, "agent_papers": 48},
    {"year": 2025, "total_papers": 200, "agent_papers": 80},
]

# データを配列として準備
years = np.array([d["year"] for d in data])
agent_counts = np.array([d["agent_papers"] for d in data])
total_counts = np.array([d["total_papers"] for d in data])

# ヒストグラムの計算(numpy.histogramを使用)
# binsを年の境界で設定
bins = np.arange(2020, 2027)
hist, bin_edges = np.histogram(years, bins=bins, weights=agent_counts)

# 棒グラフの最大長を決定(最大値に対して)
max_count = np.max(agent_counts)
max_bar_length = 20  # 最大棒の長さ

# ASCII棒グラフを出力
print("```")
for i, d in enumerate(data):
    year = d["year"]
    agent_papers = d["agent_papers"]
    total_papers = d["total_papers"]
    percentage = (agent_papers / total_papers) * 100
    
    # 棒の長さを計算(numpy使用)
    bar_length = int(np.ceil(agent_papers / max_count * max_bar_length))
    bar = "█" * bar_length
    
    print(f"{year}年 : {bar} {agent_papers}論文 ({percentage:.2f}%)")
print("```")

2025-12-03 01:04:28,759 - __main__ - INFO - Full server tool content:
2025-12-03 01:04:28,759 - __main__ - INFO -   input: {'command': 'create', 'path': '/tmp/agent_trend_analysis.py', 'file_text': 'import numpy as np\n\n# 各年のデータ\ndata = [\n    {"year": 2020, "total_papers": 200, "agent_papers": 44},\n    {"year": 2021, "total_papers": 200, "agent_papers": 36},\n    {"year": 2022, "total_papers": 200, "agent_papers": 42},\n    {"year": 2023, "total_papers": 200, "agent_papers": 44},\n    {"year": 2024, "total_papers": 200, "agent_papers": 48},\n    {"year": 2025, "total_papers": 200, "agent_papers": 80},\n]\n\n# データを配列として準備\nyears = np.array([d["year"] for d in data])\nagent_counts = np.array([d["agent_papers"] for d in data])\ntotal_counts = np.array([d["total_papers"] for d in data])\n\n# ヒストグラムの計算(numpy.histogramを使用)\n# binsを年の境界で設定\nbins = np.arange(2020, 2027)\nhist, bin_edges = np.histogram(years, bins=bins, weights=agent_counts)\n\n# 棒グラフの最大長を決定(最大値に対して)\nmax_count = np.max(agent_counts)\nmax_bar_length = 20  # 最大棒の長さ\n\n# ASCII棒グラフを出力\nprint("```")\nfor i, d in enumerate(data):\n    year = d["year"]\n    agent_papers = d["agent_papers"]\n    total_papers = d["total_papers"]\n    percentage = (agent_papers / total_papers) * 100\n    \n    # 棒の長さを計算(numpy使用)\n    bar_length = int(np.ceil(agent_papers / max_count * max_bar_length))\n    bar = "█" * bar_length\n    \n    print(f"{year}年 : {bar} {agent_papers}論文 ({percentage:.2f}%)")\nprint("```")\n'}
2025-12-03 01:04:28,759 - __main__ - INFO -   name: text_editor_code_execution
2025-12-03 01:04:28,759 - __main__ - INFO -   id: srvtoolu_01KHi1FMMLByoS8SmaHyZ6U2
2025-12-03 01:04:28,759 - __main__ - INFO -   type: server_tool_use
2025-12-03 01:04:28,759 - __main__ - INFO - ---
2025-12-03 01:04:28,759 - __main__ - INFO - Block 2 (text_editor_code_execution_tool_result):
2025-12-03 01:04:28,759 - __main__ - INFO - Tool Result Type: text_editor_code_execution_tool_result
2025-12-03 01:04:28,759 - __main__ - INFO - Tool Use ID: srvtoolu_01KHi1FMMLByoS8SmaHyZ6U2
2025-12-03 01:04:28,759 - __main__ - INFO - Content: BetaTextEditorCodeExecutionCreateResultBlock(is_file_update=False, type='text_editor_code_execution_create_result')
2025-12-03 01:04:28,759 - __main__ - INFO - ---
2025-12-03 01:04:28,759 - __main__ - INFO - Block 3 (server_tool_use):
2025-12-03 01:04:28,760 - __main__ - INFO - Server Tool: bash_code_execution
2025-12-03 01:04:28,760 - __main__ - INFO - ID: srvtoolu_01HsJ6ZAbW3h8GHPeah6fu6L
2025-12-03 01:04:28,760 - __main__ - INFO - Input: {'command': 'python /tmp/agent_trend_analysis.py'}
2025-12-03 01:04:28,760 - __main__ - INFO - Bash Command: python /tmp/agent_trend_analysis.py
2025-12-03 01:04:28,760 - __main__ - INFO - Full server tool content:
2025-12-03 01:04:28,760 - __main__ - INFO -   input: {'command': 'python /tmp/agent_trend_analysis.py'}
2025-12-03 01:04:28,760 - __main__ - INFO -   name: bash_code_execution
2025-12-03 01:04:28,760 - __main__ - INFO -   id: srvtoolu_01HsJ6ZAbW3h8GHPeah6fu6L
2025-12-03 01:04:28,760 - __main__ - INFO -   type: server_tool_use
2025-12-03 01:04:28,760 - __main__ - INFO - ---
2025-12-03 01:04:28,760 - __main__ - INFO - Block 4 (bash_code_execution_tool_result):
2025-12-03 01:04:28,760 - __main__ - INFO - Tool Result Type: bash_code_execution_tool_result
2025-12-03 01:04:28,760 - __main__ - INFO - Tool Use ID: srvtoolu_01HsJ6ZAbW3h8GHPeah6fu6L
2025-12-03 01:04:28,760 - __main__ - INFO - Content: BetaBashCodeExecutionResultBlock(content=[], return_code=0, stderr='', stdout='```\n2020年 : ███████████ 44論文 (22.00%)\n2021年 : █████████ 36論文 (18.00%)\n2022年 : ███████████ 42論文 (21.00%)\n2023年 : ███████████ 44論文 (22.00%)\n2024年 : ████████████ 48論文 (24.00%)\n2025年 : ████████████████████ 80論文 (40.00%)\n```\n', type='bash_code_execution_result')
2025-12-03 01:04:28,760 - __main__ - INFO - Stdout: 
2020: ███████████ 44論文 (22.00%)
2021: █████████ 36論文 (18.00%)
2022: ███████████ 42論文 (21.00%)
2023: ███████████ 44論文 (22.00%)
2024: ████████████ 48論文 (24.00%)
2025: ████████████████████ 80論文 (40.00%)


2025-12-03 01:04:28,760 - __main__ - INFO - ---
2025-12-03 01:04:28,760 - __main__ - INFO - Block 5 (text):
2025-12-03 01:04:28,760 - __main__ - INFO - Text: ```
2020: ███████████ 44論文 (22.00%)
2021: █████████ 36論文 (18.00%)
2022: ███████████ 42論文 (21.00%)
2023: ███████████ 44論文 (22.00%)
2024: ████████████ 48論文 (24.00%)
2025: ███████████████████...
2025-12-03 01:04:28,760 - __main__ - INFO - ---
2025-12-03 01:04:28,760 - __main__ - INFO - ==========================================
2025-12-03 01:04:28,760 - __main__ - INFO - Conversation completed
2025-12-03 01:04:28,760 - __main__ - INFO - Analysis completed successfully
2025-12-03 01:04:28,762 - __main__ - INFO - Analysis completed and saved to output.md
2025-12-03 01:04:28,762 - __main__ - INFO - Saved 219 characters

軽く解説

初期promptを書く

initial_prompt = """タスク: cs.AI論文のAgent研究増加傾向分析(2020-2025)

目的: cs.AIカテゴリの論文(2020-2025年、各年200件)のタイトル・アブストラクトを分析し、Agent研究の増加傾向を示す

実行手順:
1. 2020年から2025年まで各年でsearch_and_filter_papersツールを呼び出し:
   - 各年のcs.AI論文を200件検索
   - Agent関連キーワードでフィルタリング実行
   - 集計結果を受け取り: {"year": 2020, "total_papers": 200, "agent_papers": 10}

2. code_executionで受け取った各年の結果からASCII棒グラフ生成:
   - numpy.histogramを使用してヒストグラムデータを作成
   - 年別Agent論文数と比率を計算
   - ASCII文字(█)を使って6年間の棒グラフを描画

最終出力形式(この形式のみ):
2020年 : ███ 15論文 (7.50%)
2021年 : ████ 20論文 (10.00%)
2022年 : ██████ 30論文 (15.00%)
2023年 : ████████ 40論文 (20.00%)
2024年 : ██████████ 50論文 (25.00%)
2025年 : ████████████ 60論文 (30.00%)

重要:
- search_and_filter_papersツールを2020年から2025年で各1回ずつ(計6回)呼び出し
- code_executionでnumpyを活用:
  * import numpy as np
  * 各年のAgent論文数データを配列として作成
  * numpy.histogram()を使ってヒストグラムを計算
  * ヒストグラムの結果を基にASCII棒の長さを決定
- 受け取った結果でASCII棒グラフを作成
- 上記の形式のみを出力(他の説明や分析は不要)
- ASCII棒グラフは```コードブロックで囲む"""

どのツールを使って欲しいのかと、どんな処理をCode Execution環境で実行して欲しいかを具体的に伝えています。
Claudeがpythonのコードを書いてそれを独自のCode Execution環境で実行するので具体的に文法的なことも口出しできます。
numpy.histogram() を指定すると、ちゃんと使ってくれます。

タスク分解と任せ先を確認する

論文の抽出とフィルター(重いタスク)とフィルターされた各年ごとの論文数を集計して、棒グラフを作成する(軽いタスク)があります。
ただし、生成AIに任せるとtokenを多く(少なく)消費するタスクを重い(軽い)タスクと呼ぶことにします。

シーケンス図の通り、重いタスクは自前の提供したtoolで実行(実行環境はlocal)し、軽いタスクはCode Execution環境で実行されるようにClaude推論モデルには指示することが大事です。

実はこれが最初理解できていませんでした。
詳しくはアンチパターンの節で説明します。

勝手にpythonのコードを書くことを確認する

ログ全文に載せましたが、一部抜粋します。

2025-12-03 01:04:28,758 - __main__ - INFO - Block 1 (server_tool_use):
2025-12-03 01:04:28,758 - __main__ - INFO - Server Tool: text_editor_code_execution
2025-12-03 01:04:28,758 - __main__ - INFO - ID: srvtoolu_01KHi1FMMLByoS8SmaHyZ6U2
2025-12-03 01:04:28,759 - __main__ - INFO - Input: {'command': 'create', 'path': '/tmp/agent_trend_analysis.py', 'file_text': 'import numpy as np\n\n# 各年のデータ\ndata = [\n    {"year": 2020, "total_papers": 200, "agent_papers": 44},\n    {"year": 2021, "total_papers": 200, "agent_papers": 36},\n    {"year": 2022, "total_papers": 200, "agent_papers": 42},\n    {"year": 2023, "total_papers": 200, "agent_papers": 44},\n    {"year": 2024, "total_papers": 200, "agent_papers": 48},\n    {"year": 2025, "total_papers": 200, "agent_papers": 80},\n]\n\n# データを配列として準備\nyears = np.array([d["year"] for d in data])\nagent_counts = np.array([d["agent_papers"] for d in data])\ntotal_counts = np.array([d["total_papers"] for d in data])\n\n# ヒストグラムの計算(numpy.histogramを使用)\n# binsを年の境界で設定\nbins = np.arange(2020, 2027)\nhist, bin_edges = np.histogram(years, bins=bins, weights=agent_counts)\n\n# 棒グラフの最大長を決定(最大値に対して)\nmax_count = np.max(agent_counts)\nmax_bar_length = 20  # 最大棒の長さ\n\n# ASCII棒グラフを出力\nprint("```")\nfor i, d in enumerate(data):\n    year = d["year"]\n    agent_papers = d["agent_papers"]\n    total_papers = d["total_papers"]\n    percentage = (agent_papers / total_papers) * 100\n    \n    # 棒の長さを計算(numpy使用)\n    bar_length = int(np.ceil(agent_papers / max_count * max_bar_length))\n    bar = "█" * bar_length\n    \n    print(f"{year}年 : {bar} {agent_papers}論文 ({percentage:.2f}%)")\nprint("```")\n'}

このようにPTCを指定した場合のClaudeAPIのクライアントへのレスポンスとして、実行すべきblockの一覧が返されますが、server_tool_useというtypeのものについては、Code Execution環境でClaudeが実行すべきだと生成したPythonのscriptが乗っかってきます。
上のものが例です。指定したnp.histgramを使って集計していることを確認できます。

アンチパターン

重いタスクをCode Execution環境で実行する

例えば、例に示した、論文情報の収集と収集したabstractからAI agentに関係するかの判定をする処理は重いタスクです。これをCode Execution環境上でやろうとすると何が起こるでしょうか。
まず、そもそも、Code Execution環境上から外部に問い合わせられないのでarxiv論文の情報収集ができません。

そしてもう一つ大きな問題があります。
必ずprompt -> Claude API -> Claude推論モデルがどのツールをどの引数で実行するか判断 -> Code Execution環境上で実行すると判断された場合はPythonコードを生成 という流れを踏むので、引数判断のために200個の論文のabstract全部がcontext windowに乗ることになります。すなわちtoken消費量は大きくなります。

以前、YouTubeを無料ストレージとして使う、のような話がありました。
https://www.gizmodo.jp/2023/02/save-data-on-youtube-cloud.html

同じように、このCode Execution環境を無料の実行環境として例えば、CI/CDのように使えるのでしょうか?
これは使えません。厳密には使えるかもしれませんが、token消費は大きくなり、実質コストはかかります。
例えば、git repositoryをcloneしてきて特定のbranchでテストコードを実行するようなことをしようとすると、まず、sandbox環境なので外部に接続できずcloneに失敗します。
かといって、コードを文字列やファイルとして渡そうとすると、結局大きくtoken消費することになります。

自前で作るToolのinterfaceで受け渡しする情報は極力小さくする

Claudeのドキュメントにあるように、toolの実行結果は一度Claude推論モデルに渡されます。
そのため、仮に自前のtoolを作ったとしても、その返り値が肥大な文字列だったりすると、結局token消費量は大きくなります。
なので、集計やフィルターが最適なUseCaseだということがドキュメントには書かれています。

まとめ

以上をまとめると、Programmatic Tool Calling (PTC)の真髄は各toolが実行し出してきた結果を取りまとめて(これをOrchestrationと言っている)、一つのまとまったアウトプットを出すことです。そして、そのOrchestrationは推論モデルが生成したPythonのコードをCode Execution環境という独自の実行環境で実行することにより実現することです。

FunctionCallingと本質的に異なるものではなく、発想は同じです。
提供したToolの選択と実行の方法を生成AIが判断する部分は同じです。

その結果をまとめる部分が生成AIの生成したコードの実行によって得られると言う点が特徴です。
Orchestrationによって生成される処理はアプリケーションの実装で言うと、とても薄いUseCase層の実装のようなものだと捉えています。

Discussion