CLAUDE LABEN
FORK — Claude Code 2.1.212で/forkの挙動が変わりました。会話を新しいバックグラウンドセッションへ複製し、作業を続けたまま並走できます。従来のセッション内サブエージェントは/subtaskに移りましたLIMITS — WebSearchの呼び出しがセッション単位で既定200回に制限されました。サブエージェントの起動も既定200回が上限で、暴走した検索・委譲のループを止められますMCPBG — 2分を超えるMCPツール呼び出しは自動的にバックグラウンドへ移り、セッションが固まらなくなりました。しきい値はCLAUDE_CODE_MCP_AUTO_BACKGROUND_MSで調整できますPLANFIX — プランモードがtouchやrmといったファイルを変更するBashコマンドを、許可プロンプトもcanUseToolコールバックも通さずに実行してしまう不具合が修正されましたSONNET5 — Claude Sonnet 5は導入価格として入力100万トークンあたり2ドル、出力10ドルで提供中です。8月31日を過ぎると3ドルと15ドルに戻りますIPO — Anthropicが早ければ10月の株式公開を視野に、引受銀行が投資家との面談を組み始めたと報じられていますFORK — Claude Code 2.1.212で/forkの挙動が変わりました。会話を新しいバックグラウンドセッションへ複製し、作業を続けたまま並走できます。従来のセッション内サブエージェントは/subtaskに移りましたLIMITS — WebSearchの呼び出しがセッション単位で既定200回に制限されました。サブエージェントの起動も既定200回が上限で、暴走した検索・委譲のループを止められますMCPBG — 2分を超えるMCPツール呼び出しは自動的にバックグラウンドへ移り、セッションが固まらなくなりました。しきい値はCLAUDE_CODE_MCP_AUTO_BACKGROUND_MSで調整できますPLANFIX — プランモードがtouchやrmといったファイルを変更するBashコマンドを、許可プロンプトもcanUseToolコールバックも通さずに実行してしまう不具合が修正されましたSONNET5 — Claude Sonnet 5は導入価格として入力100万トークンあたり2ドル、出力10ドルで提供中です。8月31日を過ぎると3ドルと15ドルに戻りますIPO — Anthropicが早ければ10月の株式公開を視野に、引受銀行が投資家との面談を組み始めたと報じられています
記事一覧/API & SDK
API & SDK/2026-05-05上級

Claude API で社内ドキュメント横断検索エージェントを作る — ハイブリッドRAG・部署別アクセス制御・監査ログの本番実装

Claude API × Python で社内 Wiki・PDF・Notionを横断検索するエージェントを本番品質で実装します。ハイブリッドRAG・RBAC・監査ログの3セットを動くコードで解説。ナイーブRAGの情報漏洩リスクを防ぐ設計パターンも紹介。

RAG4Claude API115企業向け2アクセス制御監査ログ2ハイブリッド検索Python17

社内のドキュメント検索ツールを Claude API で作り始めたとき、最初に私がやったのは「とりあえずベクター検索にドキュメントを全部突っ込む」でした。おそらく多くの方が同じスタートラインに立つと思います。

そしてデモを見せたとき、人事部の担当者が「あれ、この給与データって全員が見えてるんですか?」と聞いてきました。

その瞬間、設計のやり直しが確定しました。

アクセス制御のないRAGシステムは、検索性能がいくら高くても本番には出せません。そこから3週間かけて作り直した「部署別アクセス制御付きの社内ドキュメント検索エージェント」—その実装をコードと設計の両面から共有します。

システム全体のアーキテクチャ

完成したシステムの構成は以下の通りです。

  • インジェスト層: PDF・Markdown・Notion Export → テキスト抽出 → チャンキング → エンベディング生成 → pgvector に保存
  • 検索層: ハイブリッド検索(pgvector の cos 距離 + PostgreSQL の全文検索 BM25)→ RRF でスコア統合 → RBAC フィルタリング
  • 回答生成層: フィルタリング済みチャンク → Claude API(claude-sonnet-4-6)→ 回答 + 引用元
  • 監査層: 全クエリ・回答・参照ドキュメント → 監査テーブルに記録

ポイントは「RBACをアプリ層ではなく DB 層(PostgreSQL RLS)で実装する」という設計判断です。アプリ側でフィルタリングすると、バグが情報漏洩に直結します。DB のRow Level Securityに任せることで、どんなバグがあっても見せてはいけないドキュメントは返ってこない設計にしています。

ドキュメントインジェストパイプラインの実装

まずドキュメントをベクターDBに登録する部分から実装します。

import anthropic
import psycopg2
from psycopg2.extras import execute_values
import hashlib
from pathlib import Path
from typing import Optional
import pypdf
import tiktoken
 
client = anthropic.Anthropic()
enc = tiktoken.encoding_for_model("gpt-4o")  # トークン数計測用(近似値)
 
def extract_text_from_pdf(pdf_path: str) -> str:
    """PDFからテキストを抽出する"""
    text_parts = []
    with open(pdf_path, "rb") as f:
        reader = pypdf.PdfReader(f)
        for page in reader.pages:
            text_parts.append(page.extract_text())
    return "\n".join(text_parts)
 
def chunk_document(
    text: str,
    chunk_size: int = 512,
    overlap: int = 64
) -> list[dict]:
    """
    テキストをチャンクに分割する。
    - chunk_size: トークン数(近似)
    - overlap: チャンク間の重複トークン数(文脈の継続性を保つ)
    """
    tokens = enc.encode(text)
    chunks = []
    start = 0
    
    while start < len(tokens):
        end = min(start + chunk_size, len(tokens))
        chunk_text = enc.decode(tokens[start:end])
        
        # チャンクが短すぎる場合(文書末尾)はスキップ
        if len(enc.encode(chunk_text)) < 50:
            break
        
        chunks.append({
            "text": chunk_text,
            "token_start": start,
            "token_end": end
        })
        start += chunk_size - overlap
    
    return chunks
 
def generate_embedding(text: str) -> list[float]:
    """
    Claude API でエンベディングを生成する。
    注: 2026年5月時点では Anthropic 独自のエンベディングモデルは
    提供されていないため、voyage-3-large などを使うのが現実的。
    ここでは概念実装として claude-sonnet-4-6 を使ったエンベディング生成を示す。
    実際には voyage-ai SDK に置き換えることを推奨。
    """
    # 実際の実装では voyage-ai などのエンベディングAPIを使う
    # from voyageai import Client as VoyageClient
    # voyage = VoyageClient(api_key=os.environ["VOYAGE_API_KEY"])
    # result = voyage.embed([text], model="voyage-3-large")
    # return result.embeddings[0]
    
    # デモ用: claude-sonnet-4-6 でエンベディング代替(実際には非推奨)
    raise NotImplementedError("voyage-ai などの専用エンベディングAPIを使ってください")
 
def ingest_document(
    file_path: str,
    department: str,  # "hr", "engineering", "finance", "all"
    doc_title: str,
    db_conn: psycopg2.extensions.connection
) -> int:
    """
    ドキュメントをインジェストし、チャンク数を返す。
    """
    path = Path(file_path)
    
    # テキスト抽出
    if path.suffix == ".pdf":
        text = extract_text_from_pdf(file_path)
    elif path.suffix in [".md", ".txt"]:
        text = path.read_text(encoding="utf-8")
    else:
        raise ValueError(f"未対応のファイル形式: {path.suffix}")
    
    # ドキュメントの重複チェック(ハッシュで判定)
    doc_hash = hashlib.sha256(text.encode()).hexdigest()
    
    with db_conn.cursor() as cur:
        cur.execute(
            "SELECT id FROM documents WHERE content_hash = %s",
            (doc_hash,)
        )
        if cur.fetchone():
            print(f"スキップ: {doc_title} は既にインジェスト済みです")
            return 0
    
    # チャンキング
    chunks = chunk_document(text)
    
    # DB への挿入(ドキュメント本体)
    with db_conn.cursor() as cur:
        cur.execute(
            """
            INSERT INTO documents (title, department, content_hash, file_path)
            VALUES (%s, %s, %s, %s)
            RETURNING id
            """,
            (doc_title, department, doc_hash, file_path)
        )
        doc_id = cur.fetchone()[0]
    
    # チャンクの挿入(エンベディングは別途生成)
    chunk_data = []
    for i, chunk in enumerate(chunks):
        # 実際にはここでエンベディングを生成してからDBに挿入
        # embedding = generate_embedding(chunk["text"])
        chunk_data.append((
            doc_id,
            i,
            chunk["text"],
            department,
            # embedding  # pgvector カラムへ
        ))
    
    with db_conn.cursor() as cur:
        execute_values(
            cur,
            """
            INSERT INTO document_chunks (doc_id, chunk_index, content, department)
            VALUES %s
            """,
            chunk_data
        )
    
    db_conn.commit()
    print(f"✅ インジェスト完了: {doc_title} ({len(chunks)} チャンク, 部署: {department})")
    return len(chunks)

チャンクサイズは 512 トークン・重複 64 トークンに設定しています。これより小さくすると文脈が失われ、大きくすると Claude に渡せるチャンク数が減ります。プロジェクトによって調整が必要ですが、私が試した中では 512/64 が技術ドキュメントへの適合性が高い組み合わせでした。

ハイブリッド検索(pgvector + BM25)の実装

ベクター検索だけでは「製品コード MA-2024-001 に関する仕様」のような完全一致クエリに弱いです。BM25(キーワード検索)と組み合わせることで、この問題が解消されます。

from typing import Optional
 
def hybrid_search(
    query: str,
    user_department: str,
    query_embedding: list[float],
    db_conn: psycopg2.extensions.connection,
    top_k: int = 10,
    vector_weight: float = 0.6,
    bm25_weight: float = 0.4
) -> list[dict]:
    """
    ハイブリッド検索(RRF: Reciprocal Rank Fusion)。
    - pgvector で上位 k 件取得
    - PostgreSQL 全文検索(tsvector)で上位 k 件取得
    - RRF スコアで統合・再ランク
    - RLS により user_department でフィルタリング(DB 層で保証)
    
    重要: この関数は DB RLS のロールに user_department が設定された
    接続で呼び出すこと。アプリ層でのフィルタリングは行わない。
    """
    rrf_k = 60  # RRF のパラメータ(一般的に 60 が推奨値)
    
    with db_conn.cursor() as cur:
        # ① ベクター検索(RLS が自動で department フィルタを適用)
        cur.execute(
            """
            SELECT id, content, doc_id, chunk_index,
                   1 - (embedding <=> %s::vector) AS score
            FROM document_chunks
            ORDER BY embedding <=> %s::vector
            LIMIT %s
            """,
            (query_embedding, query_embedding, top_k * 2)
        )
        vector_results = {row[0]: (i, row) for i, row in enumerate(cur.fetchall())}
        
        # ② BM25 全文検索
        cur.execute(
            """
            SELECT id, content, doc_id, chunk_index,
                   ts_rank(to_tsvector('japanese', content),
                           plainto_tsquery('japanese', %s)) AS score
            FROM document_chunks
            WHERE to_tsvector('japanese', content) @@ plainto_tsquery('japanese', %s)
            ORDER BY score DESC
            LIMIT %s
            """,
            (query, query, top_k * 2)
        )
        bm25_results = {row[0]: (i, row) for i, row in enumerate(cur.fetchall())}
    
    # ③ RRF スコアで統合
    all_ids = set(vector_results.keys()) | set(bm25_results.keys())
    scored_results = []
    
    for chunk_id in all_ids:
        rrf_score = 0.0
        row_data = None
        
        if chunk_id in vector_results:
            rank, row = vector_results[chunk_id]
            rrf_score += vector_weight / (rrf_k + rank + 1)
            row_data = row
        
        if chunk_id in bm25_results:
            rank, row = bm25_results[chunk_id]
            rrf_score += bm25_weight / (rrf_k + rank + 1)
            if row_data is None:
                row_data = row
        
        scored_results.append({
            "id": chunk_id,
            "content": row_data[1],
            "doc_id": row_data[2],
            "chunk_index": row_data[3],
            "rrf_score": rrf_score
        })
    
    # スコア降順でソートして上位 k 件を返す
    scored_results.sort(key=lambda x: x["rrf_score"], reverse=True)
    return scored_results[:top_k]

RRF(Reciprocal Rank Fusion)の rrf_k = 60 は経験則的な値で、検索論文でよく使われるパラメータです。ベクター検索と BM25 の重みは 0.6/0.4 にしていますが、日本語技術ドキュメントではキーワード検索の比重をもう少し上げてもいいかもしれません。

部署別アクセス制御(RBAC)の実装

これが設計の核心部分です。PostgreSQL の Row Level Security(RLS)を使います。

-- テーブル定義
CREATE TABLE document_chunks (
    id          BIGSERIAL PRIMARY KEY,
    doc_id      BIGINT NOT NULL REFERENCES documents(id),
    chunk_index INT NOT NULL,
    content     TEXT NOT NULL,
    department  TEXT NOT NULL,  -- "hr", "engineering", "finance", "all"
    embedding   vector(1024)    -- voyage-3-large の次元数
);
 
-- RLS の有効化
ALTER TABLE document_chunks ENABLE ROW LEVEL SECURITY;
ALTER TABLE document_chunks FORCE ROW LEVEL SECURITY;
 
-- ポリシー定義
-- ① 自分の部署のドキュメントは見られる
CREATE POLICY dept_access ON document_chunks
    USING (
        department = current_setting('app.user_department')
        OR department = 'all'
    );
 
-- ② 管理者はすべて見られる
CREATE POLICY admin_access ON document_chunks
    TO admin_role
    USING (true);
 
-- アプリ用ロール(一般ユーザー)
CREATE ROLE app_user;
GRANT SELECT ON document_chunks TO app_user;
import psycopg2
import os
from contextlib import contextmanager
 
@contextmanager
def get_db_connection_for_user(user_department: str):
    """
    ユーザーの部署に対応した DB 接続を返すコンテキストマネージャ。
    RLS が有効なため、この接続では user_department 以外のデータは見えない。
    """
    conn = psycopg2.connect(
        dsn=os.environ["DATABASE_URL"],
        options=f"-c role=app_user"  # アプリ用ロールに切り替え
    )
    try:
        with conn.cursor() as cur:
            # セッション変数にユーザーの部署をセット(RLS が参照する)
            cur.execute(
                "SELECT set_config('app.user_department', %s, true)",
                (user_department,)
            )
        yield conn
    finally:
        conn.close()
 
# 使用例
async def search_documents(query: str, user_info: dict) -> list[dict]:
    """
    ユーザーの部署に基づいてアクセス制御された検索を実行する。
    """
    with get_db_connection_for_user(user_info["department"]) as conn:
        # この conn 経由のクエリは RLS によって自動フィルタリングされる
        # アプリ層での department フィルタは不要(というより追加しても意味がない)
        results = hybrid_search(
            query=query,
            user_department=user_info["department"],
            query_embedding=await generate_embedding_async(query),
            db_conn=conn,
            top_k=10
        )
    return results

RLS の落とし穴: set_config の第3引数を true(トランザクション内でのみ有効)ではなく false(セッション全体で有効)にすると、接続プールを使っているときに設定が他のユーザーに漏れる危険があります。必ず true を使ってください。

Claude API を使った QA エージェントの実装

フィルタリング済みのチャンクを Claude に渡して回答を生成します。

import anthropic
import json
from datetime import datetime, timezone
 
client = anthropic.Anthropic()
 
SYSTEM_PROMPT = """あなたは社内ドキュメントの検索・回答エージェントです。
 
以下のルールを厳守してください:
1. 提供されたドキュメントチャンクのみに基づいて回答する
2. チャンクに記載のない情報は「この情報は提供されたドキュメントには含まれていません」と明示する
3. 回答の根拠となるドキュメントを必ず引用元として示す
4. 機密情報を含む可能性がある場合は、回答の末尾に注記を入れる
 
回答形式:
- 回答本文
- 引用元: [ドキュメント名, チャンクインデックス]
"""
 
def format_chunks_for_context(chunks: list[dict]) -> str:
    """チャンクを Claude に渡すコンテキストとしてフォーマットする"""
    formatted = []
    for i, chunk in enumerate(chunks):
        formatted.append(
            f"[チャンク {i+1}] (ドキュメントID: {chunk['doc_id']}, "
            f"位置: {chunk['chunk_index']})\n{chunk['content']}"
        )
    return "\n\n---\n\n".join(formatted)
 
def answer_query(
    query: str,
    chunks: list[dict],
    user_info: dict
) -> dict:
    """
    検索されたチャンクを基に Claude で回答を生成する。
    
    Returns:
        {
            "answer": str,
            "cited_chunks": list[int],
            "model": str,
            "input_tokens": int,
            "output_tokens": int
        }
    """
    if not chunks:
        return {
            "answer": "お探しのドキュメントが見つかりませんでした。"
                      "検索キーワードを変えるか、アクセス権限をご確認ください。",
            "cited_chunks": [],
            "model": "none",
            "input_tokens": 0,
            "output_tokens": 0
        }
    
    context = format_chunks_for_context(chunks)
    
    # コンテキスト長の確認(claude-sonnet-4-6 は 200K トークン)
    # チャンクが多すぎる場合は上位 5 件に絞る
    if len(chunks) > 8:
        chunks = chunks[:8]
        context = format_chunks_for_context(chunks)
    
    response = client.messages.create(
        model="claude-sonnet-4-6",
        max_tokens=2048,
        system=SYSTEM_PROMPT,
        messages=[
            {
                "role": "user",
                "content": f"以下のドキュメントチャンクを参照して、質問に答えてください。\n\n"
                           f"## ドキュメントチャンク\n\n{context}\n\n"
                           f"## 質問\n\n{query}"
            }
        ]
    )
    
    return {
        "answer": response.content[0].text,
        "cited_chunks": [c["id"] for c in chunks],
        "model": response.model,
        "input_tokens": response.usage.input_tokens,
        "output_tokens": response.usage.output_tokens
    }

監査ログとコンプライアンス対応

SOC2 や社内コンプライアンスのために、すべての検索と回答を記録します。

import uuid
from datetime import datetime, timezone
 
def log_search_event(
    db_conn: psycopg2.extensions.connection,
    user_id: str,
    user_department: str,
    query: str,
    cited_chunk_ids: list[int],
    answer_summary: str,  # 回答の最初の200文字(全文保存はコスト増)
    model: str,
    input_tokens: int,
    output_tokens: int,
    session_id: Optional[str] = None
) -> str:
    """
    監査ログに検索イベントを記録する。
    返り値: イベントID(UUID)
    """
    event_id = str(uuid.uuid4())
    
    with db_conn.cursor() as cur:
        cur.execute(
            """
            INSERT INTO search_audit_log (
                event_id,
                user_id,
                user_department,
                query_text,
                cited_chunk_ids,
                answer_summary,
                model_used,
                input_tokens,
                output_tokens,
                session_id,
                created_at
            ) VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s)
            """,
            (
                event_id,
                user_id,
                user_department,
                query,
                cited_chunk_ids,  # PostgreSQL の integer[] 型
                answer_summary[:200],
                model,
                input_tokens,
                output_tokens,
                session_id,
                datetime.now(timezone.utc)
            )
        )
        db_conn.commit()
    
    return event_id
 
# 月次コストレポートのクエリ例
MONTHLY_COST_QUERY = """
SELECT
    DATE_TRUNC('day', created_at) AS day,
    user_department,
    COUNT(*) AS query_count,
    SUM(input_tokens) AS total_input_tokens,
    SUM(output_tokens) AS total_output_tokens,
    -- claude-sonnet-4-6 の価格: input $3/M, output $15/M(2026-05時点の近似)
    ROUND(
        (SUM(input_tokens) * 3.0 + SUM(output_tokens) * 15.0) / 1000000,
        4
    ) AS estimated_cost_usd
FROM search_audit_log
WHERE created_at >= DATE_TRUNC('month', CURRENT_DATE)
GROUP BY 1, 2
ORDER BY 1, 2;
"""

監査テーブルは別スキーマ(audit)に置き、アプリユーザーは INSERT のみ可能にする設計を推奨します。このテーブルへの SELECT は監査担当者ロールのみに限定することで、ログの改ざんリスクを最小化できます。

よくある失敗と落とし穴

落とし穴1: チャンクサイズが大きすぎる

最初は「コンテキストが多い方がいいはず」と思って 2048 トークンのチャンクにしていました。その結果、Claude に渡せるチャンク数が 2〜3 件に絞られ、関連情報を取りこぼすケースが頻発しました。

512 トークンに変えたところ、同じコンテキスト予算で 8〜10 件のチャンクを渡せるようになり、回答の精度が明らかに上がりました。チャンクは小さめにして数を増やす方向が私の経験上は良好です。

# ❌ これは避けてください
chunk_size = 2048  # 大きすぎる → チャンク数が少なくなる
 
# ✅ 推奨
chunk_size = 512   # Claude への入力チャンク数を 8〜10 件確保できる
overlap = 64       # 文脈の継続性を保つ重複

落とし穴2: RLS を FORCE ROW LEVEL SECURITY にしていない

デフォルトでは、テーブルのオーナーロールは RLS をバイパスします。FORCE ROW LEVEL SECURITY を設定していないと、開発中に管理者接続でテストすると「全データが見えて正常」と誤認し、本番で一般ユーザーとして接続したときに初めてバグが発覚します。

-- ❌ これだとオーナーロールはバイパスできてしまう
ALTER TABLE document_chunks ENABLE ROW LEVEL SECURITY;
 
-- ✅ 全ロールに RLS を強制する
ALTER TABLE document_chunks ENABLE ROW LEVEL SECURITY;
ALTER TABLE document_chunks FORCE ROW LEVEL SECURITY;

落とし穴3: エンベディングの更新を忘れる

ドキュメントを更新したとき、テキストだけ新しくしてエンベディングを再生成しないケースが発生しました。テキストとエンベディングがずれているチャンクは、ベクター検索で意味不明な結果を返します。

def update_document(doc_id: int, new_text: str, db_conn):
    """ドキュメント更新時は必ずエンベディングも再生成する"""
    new_hash = hashlib.sha256(new_text.encode()).hexdigest()
    chunks = chunk_document(new_text)
    
    with db_conn.cursor() as cur:
        # 古いチャンクを削除
        cur.execute("DELETE FROM document_chunks WHERE doc_id = %s", (doc_id,))
        
        # 新しいチャンクとエンベディングを挿入
        for i, chunk in enumerate(chunks):
            # embedding = generate_embedding(chunk["text"])  # 必ず再生成
            cur.execute(
                """
                INSERT INTO document_chunks (doc_id, chunk_index, content, department)
                VALUES (%s, %s, %s, (SELECT department FROM documents WHERE id = %s))
                """,
                (doc_id, i, chunk["text"], doc_id)
            )
        
        # ドキュメントのハッシュも更新
        cur.execute(
            "UPDATE documents SET content_hash = %s, updated_at = NOW() WHERE id = %s",
            (new_hash, doc_id)
        )
    
    db_conn.commit()

落とし穴4: 日本語テキストの全文検索設定

PostgreSQL の to_tsvector('japanese', ...)pg_bigmpgroonga 拡張が必要です。デフォルトの simpleenglish では日本語の形態素解析が行われず、BM25 スコアがほぼゼロになります。

-- pgroonga 拡張をインストール(Supabase なら Extensions から有効化)
CREATE EXTENSION IF NOT EXISTS pgroonga;
 
-- pgroonga インデックスを使った日本語全文検索
CREATE INDEX idx_chunks_content_pgroonga
    ON document_chunks
    USING pgroonga (content pgroonga_text_full_text_search_ops_v2);

この先の発展方向

今回実装したシステムで、私のチームでは次の課題が見えてきました。参考までに共有します。

チャンク間の関係性: 現状は独立したチャンクとして扱っていますが、「このチャンクはどのセクションに属するか」というメタデータを持たせると、回答の文脈がより豊かになります。階層的なチャンキング(大きなチャンクで意味的なまとまりを保ち、小さなチャンクで詳細を検索する)は試す価値があります。

再ランキング: BM25 + ベクターの RRF だけでは不十分なケースもあります。Cohere Rerank や cross-encoder モデルで最終的な並び替えを入れると、特にドキュメントが多い環境での精度が向上します。

キャッシュ: 同じクエリに対するエンベディング生成と検索結果をキャッシュするだけで、API コストと応答時間が大幅に改善されました。Redis に 1 時間 TTL でキャッシュするのが手軽でおすすめです。

まずは小規模な社内ドキュメント(10〜100ファイル)でシステム全体を動かしてみてください。RLS のポリシーが意図通りに機能しているか、部署をまたいだアクセスが本当にブロックされているかをテストから始めるのが、本番投入への最短ルートです。

RLS の仕組みを深く理解する上でも、RDBMSの根本的な設計思想を把握していると応用が利きます。

シェア

お読みいただきありがとうございます

Claude Lab は広告なしで運営しており、サーバー費用などの運営コストはメンバーシップのご支援で賄っています。実装コード・ベンチマーク・本番設計パターンなど、実務でお役立ていただける記事を毎日更新しています。もし読んでよかったと感じていただけましたら、ぜひご覧ください。

  • コピー&ペーストで使える実装コード付き
  • 毎日新しい上級ガイドを追加
  • ¥580/月 または ¥1,480 の永久アクセス
メンバーシップを見る →

もしこの記事がお役に立ちましたら、チップ(¥150)で応援いただけると大変励みになります。広告なしでの運営を続けるため、皆さまのご支援が大きな力になっています。

関連記事

API & SDK2026-07-09
RAG が自信たっぷりに間違え始めたとき — 検索の取りこぼしを接地率で計測する運用メモ
Claude API の RAG で、回答は流暢なのに事実がずれ始める。多くの場合、原因は検索側で答えを含む文書を取りこぼすサイレントなリコール低下です。接地率と検索ヒット率を計測し、段階的に立て直した運用メモを、動くコードと実数値付きで残します。
API & SDK2026-06-25
API リクエスト1本でリモート MCP サーバーに直接つなぐ — Messages API の MCP コネクタ実装メモ
ローカルに MCP クライアントを立てずに、Messages API の mcp_servers と mcp_toolset だけでリモート MCP サーバーのツールを呼ぶ実装をまとめました。allowlist/denylist 設計、レスポンス処理、無人運用での落とし穴まで。
API & SDK2026-06-19
search_result ブロックで自前ナレッジに根拠を引かせる — Claude Citations 実装メモ
自前のRAGでClaudeが「どの記事を根拠にしたか」を見失う問題を、search_result コンテンツブロックと citations で解決する実装手順を、4サイト運用の実測値とともにまとめます。
📚RECOMMENDED BOOKS
大規模言語モデル入門
山田育矢
LLM開発
生成AIプロンプトエンジニアリング入門
我妻幸長
プロンプト
Claude CodeによるAI駆動開発入門
平川知秀
AI駆動開発
※ アフィリエイトリンクを含みます
もっと見る →