Perplexity Agent APIの引用番号を出典リンクへ戻す手順

複数の検索結果イベントを収集し、結果IDで引用を対応付け、未解決番号と未完了応答を残す方法。

AIものさし編集部

Perplexity Agent APIの回答に引用番号があるのにリンクへ戻れない場合は、回答文だけでなく、検索結果を最後まで集めているかを確認します。最初の検索結果だけを保存したり、配列の順番で番号を振り直したりすると、後の検索で使われた出典との対応を失います。

この記事は、検索を伴うAgent APIのストリーミング応答を使い、引用番号と出典を対応付ける手順です。Sonarの応答形式やSearch API単体の実装には、そのまま適用しません。実APIの呼び出しは行っていません。掲載コードは架空イベントを処理するローカル例で、引用先の内容やリンク到達を実測したものではありません。

回答文と検索結果を、別に集める

公式のStreaming Citation Parsingは、検索結果をresponse.reasoning.search_results、回答文の増分をresponse.output_text.deltaというイベントで説明しています。検索が複数回あれば検索結果も複数のまとまりで届き、全体で共通の引用IDを使います。各イベントの結果を集め、結果にあるidで本文の番号と結び付けます。Perplexity:Streaming Citation Parsing(新しいタブで開く)

ここで保存するものは、受け取った回答文、全検索結果、応答の完了状態です。一つの変数へ最後の検索結果だけを代入して終わらないようにします。回答文が表示され始めたことだけで、出典一覧も完成したとは扱いません。

出典URLはモデルへ文章として書き直させず、応答の構造化された検索結果から取得します。公式Prompt Guideも、文章中のURLではなくsearch_resultsのresults内にあるurlを参照するよう説明しています。引用の表記は設定によって番号形式やインラインリンクなどが異なるため、利用する出力形式を先に確認してください。Perplexity:Prompt Guide(新しいタブで開く)

配列の位置を引用IDとして使わない

次は、二回の検索から結果を受け取ったと想定する架空例です。IDを連番にしていないのは、配列位置を使う誤りを見つけやすくするためです。

表は横にスクロールできます

到着したまとまり結果のid結果のurl
一回目の検索2https://example.com/source-a(新しいタブで開く)
二回目の検索9https://example.com/source-b(新しいタブで開く)

本文が「条件A[2]、条件B[9]」なら、2をsource-a、9をsource-bへ対応させます。最初の結果だから1、二つ目だから2と付け直しません。検索結果を日付やタイトルで並べ替える場合も、元のidを保持します。

同じ応答に属する結果を集めることも必要です。別の利用者や別の要求の結果を共通の辞書へ混ぜると、同じ番号でも違うURLへ結び付く可能性があります。応答ごとに収集領域を分けてください。

分割された引用番号は、文章を蓄積してから検査する

回答文の増分が引用記号の境界で区切られるとは前提にしません。例えば、ある増分が「条件B[」、次が「9]」なら、個々の増分だけを調べても[9]を見つけられません。

次のPythonは、辞書に整えた架空イベントを入力とし、本文と検索結果を集める例です。対象は[数字]形式だけです。[web:数字]、Markdown全般、SDKオブジェクト、SSEの通信処理を扱う完全なクライアントではありません。

import re


def collect_citations(events):
    text_parts = []
    sources = {}
    completed = False

    for event in events:
        kind = event.get("type")
        if kind == "response.reasoning.search_results":
            for result in event.get("results") or []:
                ref = str(result["id"])
                url = result["url"]
                if ref in sources and sources[ref] != url:
                    raise ValueError("conflicting citation id")
                sources[ref] = url
        elif kind == "response.output_text.delta":
            text_parts.append(event["delta"])
        elif kind == "response.completed":
            completed = True

    text = "".join(text_parts)
    refs = set(re.findall(r"\[(\d+)\]", text))
    return {
        "text": text,
        "links": {ref: sources[ref] for ref in sorted(refs) if ref in sources},
        "unresolved": sorted(refs - sources.keys()),
        "completed": completed,
    }

この関数の重複IDに対する例外は、異なるURLを黙って上書きしないために置いた実装上の判断です。APIが重複IDを返すという主張ではありません。異常を記録して原因を調べる設計にし、都合のよい方を選ばないようにします。

また、正規表現は文章中の[数字]をすべて候補として拾います。本文に引用以外の番号がある場合、それも候補になるため、製品の引用形式と用途に合わせた処理が別途必要です。見つかった数字がすべて正しい出典を意味するとは保証しません。

未解決の番号と、未完了の応答を隠さない

架空の本文を「条件A[2]、条件B[9]、条件C[8]」とし、検索結果に2と9だけがあれば、この例の未解決番号は8です。8を最も近いURLへ付け替えたり、出典がそろった回答として保存したりしません。

検算では次の条件を分けます。

表は横にスクロールできます

入力条件確認する結果
二回の検索結果が届く2と9の両方を保持する
[9]の途中で文章が分かれる蓄積後に9を認識する
本文に[8]、検索結果に8なし未解決として8を残す
完了イベントなしcompletedをfalseのまま残す
同じIDへ異なるURLを入れるこの例では例外にする

公式Output Controlの例はresponse.completedを完了のイベントとして扱います。通信が途中で終わったことと、応答が完了したことは分けて記録してください。Perplexity:Output Control(新しいタブで開く)

この関数は完了イベントの有無だけを返すため、エラーイベントの内容を分類したり、通信切断を再接続したりはしません。実装では元の応答識別情報と失敗記録も保持し、部分回答を正常完了の結果と取り違えないようにします。

リンクを解決した後で、根拠として読めるかを確認する

番号からURLへ戻れたことは、主張が正しい証拠とは別です。例えば、条件Bの出典が別年度の資料なら、リンクの対応は正しくても、その条件を支えない可能性があります。引用先を読み、対象、時点、条件、本文の主張と一致するかを確認します。

同様に、検索結果にあるURLを取得できたことだけで、社内画面に安全に埋め込めると判断しません。表示時はタイトルやURLをデータとして扱い、利用する画面のエスケープやリンク方針に従います。掲載関数にはHTML生成やURL検証の実装は含めていません。

引き継ぎには、対象API形式と引用表記、収集した検索結果のまとまり、未解決番号、応答完了の有無、内容確認が必要な主張を残します。回答文の表示、引用リンクの解決、根拠内容の確認を分けると、どこで不足したかを調べやすくなります。

参考資料

公式資料の確認日:2026年10月23日。番号・URL・イベントは説明用の架空データです。