
Pi Agent 実践ガイド
Pi Extension を使って DeepSearch 機能を実装する:Pi が問題を分解し、資料を検索し、証拠を整理し、情報源付きの研究結果を生成できるようにする
クイックレビュー
概念編では、Pi Agent を極めてシンプルな Agent Harness と理解しました。これはモデル、ターミナル、ファイルシステム、シェル、セッション、拡張システムを接続しますが、重厚なワークフローを事前に設定することはありません。
そのため、実践編では一般的な「Pi にファイルを変更させる」というケースは避けたいと思います。そのケースは基本的な閉ループを説明できますが、Pi の拡張性を十分に示せません。
Pi にとってより適切な実践ケースは、デフォルトでは持っていないが多くの人が実際に必要としている機能、DeepSearch を追加することです。
ここでの DeepSearch は単なるインターネット検索ではなく、研究型のワークフローです。
| フェーズ | 内容 |
|---|---|
| 問題分解 | 曖昧な問題を検索可能な複数のサブ問題に分解する |
| 多段階検索 | 公式ドキュメント、コードリポジトリ、ブログ、ディスカッションフォーラム、論文をそれぞれ検索する |
| 情報源のフィルタリング | 重複排除、低品質な結果の除外、一次情報源の優先 |
| 証拠の整理 | 重要な事実、リンク、時間、バージョン、不確実性を抽出する |
| 総合的な回答 | 結論を提示し、その根拠と制約を説明する |
私の判断では、DeepSearch は Pi 本体に組み込むべきではなく、プロンプトだけで無理やり実現すべきでもありません。Pi Extension として作成するのがより適切です。
理由は簡単です。DeepSearch にはネットワークリクエスト、サードパーティの検索 API、情報源のフィルタリング、結果の切り詰め、引用形式、セキュリティ境界が関わってきます。これらはすべてワークフロー機能であり、コーディングエージェントの最小限の核ではありません。
設計目標
このケースで実現するのは完璧な研究システムではなく、動作する最小バージョンです。
目標は以下の通りです。
Pi に deep_search ツールを追加する。
これは以下を受け取ります。
- query:ユーザーが調査したい問題
- depth:検索深度
- maxResults:返却する候補資料の最大数
これは以下を出力します。
- 構造化された検索結果
- 各結果のタイトル、URL、要約、関連性
- モデルが使用するための証拠プロンプト
Pi はこれらの証拠を受け取った後、現在のモデルによって最終的な結論を生成します。意図的に「検索」と「総合」を分離します。
| 部分 | 担当者 | 理由 |
|---|---|---|
| 検索 API 呼び出し | DeepSearch extension | これは確定的な外部機能です |
| 結果の重複排除と切り詰め | DeepSearch extension | コンテキストがノイズで埋め尽くされるのを防ぎます |
| どの証拠が重要かの判断 | Pi の現在のモデル | 推論とコンテキスト理解が必要です |
| 最終的な回答の作成 | Pi の現在のモデル | ユーザーの質問とプロジェクトのコンテキストを組み合わせる必要があります |
このようにすることで、より安定します。Extension はそれ自体でモデルを呼び出す必要はなく、ネストされたエージェントになる必要もありません。高品質な証拠を提供するだけで、Pi の元のモデルが推論を続けます。
準備作業
Pi extension はグローバルディレクトリに置くことも、プロジェクトディレクトリに置くこともできます。ここではまずプロジェクトディレクトリに置くことをお勧めします。
.pi/extensions/deepsearch/
package.json
index.tsプロジェクトローカルの extension の利点は、境界が明確であることです。この DeepSearch 機能は現在のプロジェクトでのみ有効になり、すべての Pi セッションに影響を与えません。
検索サービスは Tavily、Exa、Brave Search、SerpAPI、あるいは独自の検索バックエンドを選択できます。最初のバージョンではサービスプロバイダーにこだわらず、まず searchWeb() 関数として抽象化します。
例えば、環境変数に API キーを保存します。
export TAVILY_API_KEY=tvly-...サードパーティの検索 API を接続したくない場合は、まずローカルのモックデータを使用して extension を動作させることもできます。ツールの登録、パラメータの受け渡し、結果の形式が安定した後で、実際の検索サービスに接続します。
Step 1: Extension ディレクトリの作成
まずディレクトリを作成します。
mkdir -p .pi/extensions/deepsearchextension が依存関係を必要とする場合、package.json を置くことができます。
{
"name": "pi-deepsearch-extension",
"private": true,
"dependencies": {
"typebox": "*",
"@earendil-works/pi-ai": "*",
"@earendil-works/pi-coding-agent": "*"
},
"pi": {
"extensions": ["./index.ts"]
}
}次に依存関係をインストールします。
cd .pi/extensions/deepsearch
npm installPi の extension は TypeScript モジュールであり、手動でコンパイルする必要はありません。この体験はツールの迅速な実験に適しています。
Step 2: deep_search ツールの登録
コアファイルは .pi/extensions/deepsearch/index.ts です。
最初のバージョンは次のように書けます。
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
import { StringEnum } from "@earendil-works/pi-ai";
import { Type } from "typebox";
type SearchResult = {
title: string;
url: string;
snippet: string;
score?: number;
};
export default function (pi: ExtensionAPI) {
pi.registerTool({
name: "deep_search",
label: "DeepSearch",
description: "Search the web for source-backed evidence about a question.",
promptSnippet: "Research a question with web search and return source-backed evidence.",
promptGuidelines: [
"Use deep_search when the user asks for current facts, external sources, comparison, investigation, or source-backed research.",
"After deep_search returns results, synthesize an answer with citations and clearly separate facts, inference, and uncertainty.",
"Do not treat deep_search results as final truth; inspect source quality and mention gaps."
],
parameters: Type.Object({
query: Type.String({
description: "The research question or search query."
}),
depth: Type.Optional(StringEnum(["quick", "normal", "deep"] as const)),
maxResults: Type.Optional(Type.Number({
minimum: 3,
maximum: 10,
default: 6
}))
}),
async execute(_toolCallId, params, signal) {
const depth = params.depth ?? "normal";
const maxResults = params.maxResults ?? 6;
const results = await searchWeb(params.query, depth, maxResults, signal);
return {
content: [
{
type: "text",
text: formatResultsForModel(params.query, results)
}
],
details: {
query: params.query,
depth,
results
}
};
}
});
}
async function searchWeb(
query: string,
depth: "quick" | "normal" | "deep",
maxResults: number,
signal: AbortSignal
): Promise<SearchResult[]> {
const apiKey = process.env.TAVILY_API_KEY;
if (!apiKey) {
throw new Error("Missing TAVILY_API_KEY. Set it before starting pi.");
}
const response = await fetch("https://api.tavily.com/search", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
api_key: apiKey,
query,
search_depth: depth === "quick" ? "basic" : "advanced",
max_results: maxResults,
include_answer: false,
include_raw_content: depth === "deep"
}),
signal
});
if (!response.ok) {
throw new Error(`Search failed: ${response.status} ${response.statusText}`);
}
const data = await response.json() as {
results?: Array<{
title?: string;
url?: string;
content?: string;
score?: number;
}>;
};
return dedupeByUrl((data.results ?? []).map((item) => ({
title: item.title ?? "Untitled",
url: item.url ?? "",
snippet: item.content ?? "",
score: item.score
}))).filter((item) => item.url);
}
function dedupeByUrl(results: SearchResult[]): SearchResult[] {
const seen = new Set<string>();
const deduped: SearchResult[] = [];
for (const result of results) {
const key = normalizeUrl(result.url);
if (seen.has(key)) continue;
seen.add(key);
deduped.push(result);
}
return deduped;
}
function normalizeUrl(url: string): string {
try {
const parsed = new URL(url);
parsed.hash = "";
parsed.searchParams.delete("utm_source");
parsed.searchParams.delete("utm_medium");
parsed.searchParams.delete("utm_campaign");
return parsed.toString();
} catch {
return url;
}
}
function formatResultsForModel(query: string, results: SearchResult[]): string {
if (results.length === 0) {
return `DeepSearch found no results for: ${query}`;
}
const lines = results.map((result, index) => {
return [
`## Source ${index + 1}`,
`Title: ${result.title}`,
`URL: ${result.url}`,
result.score === undefined ? undefined : `Score: ${result.score}`,
`Snippet: ${result.snippet}`
].filter(Boolean).join("\n");
});
return [
`DeepSearch query: ${query}`,
"",
"Use these sources as evidence. Cite URLs when making factual claims.",
"Separate confirmed facts from inference and uncertainty.",
"",
...lines
].join("\n\n");
}このコードは最も重要なことだけを行います。
| コード位置 | 役割 |
|---|---|
pi.registerTool() | deep_search をモデル呼び出しに公開する |
parameters | ツールが必要とするパラメータをモデルに伝える |
promptGuidelines | いつ使用し、使用後にどのように処理するかをモデルに伝える |
searchWeb() | 実際の検索サービスを呼び出す |
dedupeByUrl() | 重複する URL を削除する |
formatResultsForModel() | 検索結果をモデルが引用しやすい証拠ブロックに整理する |
最初のバージョンでは複雑にしすぎないでください。DeepSearch の本当の難しさは、検索リクエストを書くことではなく、情報源の品質、コンテキストの長さ、引用形式、不確実性を制御することです。
Step 3: /deepsearch コマンドの追加
ツールはモデルが呼び出すものですが、ユーザーも直接アクセスできる入り口が必要です。
別のコマンドを登録して、ユーザー入力をより明確な研究タスクに書き換えることができます。
export default function (pi: ExtensionAPI) {
pi.registerCommand("deepsearch", {
description: "Run a source-backed DeepSearch task",
handler: async (args, ctx) => {
const query = String(args ?? "").trim();
if (!query) {
ctx.ui.notify("Usage: /deepsearch <question>", "warning");
return;
}
pi.sendUserMessage(
[
"以下の質問について DeepSearch を実行してください。",
"",
`質問:${query}`,
"",
"要件:",
"1. まず deep_search を呼び出す必要があるかどうかを判断してください。",
"2. 問題が複雑な場合は、2〜4つのサブ問題に分解してそれぞれ検索してください。",
"3. 最終的な回答には情報源のリンクを含める必要があります。",
"4. 事実、推論、まだ不確実な部分を区別してください。",
"5. 検索結果をそのまま羅列するのではなく、総合的な判断を示してください。"
].join("\n"),
{ deliverAs: "followUp" }
);
}
});
pi.registerTool({
// deep_search tool definition...
});
}これにより、ユーザーは直接次のように入力できます。
/deepsearch Pi Coding Agent の extension メカニズムはどのような機能に適していますか?/deepsearch は直接検索するのではなく、Pi により完全なタスク説明を送信します。モデルは説明に基づいて deep_search を呼び出し、その結果に基づいて総合的な回答を完成させます。
私はこの設計の方が好きです。なぜなら、エージェントの判断の余地を残しているからです。検索ツールは証拠の入り口に過ぎず、最終的な回答生成器ではありません。
Step 4: 起動と検証
プロジェクトローカルの extension を配置したら、プロジェクトのルートディレクトリで Pi を直接起動できます。
TAVILY_API_KEY=tvly-... pi一時的にテストするだけであれば、明示的に extension を指定することもできます。
TAVILY_API_KEY=tvly-... pi -e ./.pi/extensions/deepsearch/index.tsPi に入ったら、まず外部の事実を必要とする質問をします。
/deepsearch Pi Coding Agent の最新バージョンの extension システムはどのような機能をサポートしていますか?許容される出力は、単なる検索結果のリストではなく、以下を含むべきです。
| チェックポイント | 適切なパフォーマンス |
|---|---|
| ツールが呼び出されたか | deep_search が呼び出されたことが確認できる |
| 情報源が明確か | 各重要な事実の後に URL がある |
| 重複排除されているか | 同じページを繰り返し引用しない |
| 判断があるか | 資料を羅列するだけでなく、適用シナリオを要約できる |
| 不確実性があるか | バージョン変更、サードパーティ API、コミュニティ拡張に対して境界を保つ |
結果が単なる「検索結果リスト」である場合、promptGuidelines が十分に強力ではないことを示しています。ガイドラインをより明確にすることができます。
promptGuidelines: [
"Use deep_search to gather evidence, not to produce the final answer.",
"After deep_search, write a concise research brief with citations.",
"Prefer official documentation, source code, release notes, and primary sources.",
"Mention when sources disagree or when the evidence is incomplete."
]Step 5: DeepSearch をより研究ツールらしくする
最初のバージョンが動作したら、さらに3種類の機能を追加できます。
サブ問題の分解
DeepSearch が最も失敗しやすいのは、大きな問題を直接検索 API に投げ込むことです。
例えば:
Pi Agent は Claude Code の代替になりえますか?これは良い検索クエリではありません。少なくとも次のように分解できます。
| サブ問題 | 役割 |
|---|---|
| Pi Agent のコア設計は何ですか | 位置付けを探す |
| Pi Agent はどのようなツールと拡張機能をサポートしていますか | 機能の境界を探す |
| Claude Code のデフォルト機能は何ですか | 比較対象を探す |
| 両者の権限、セキュリティ、拡張性にはどのような違いがありますか | 判断を形成する |
最初のバージョンではモデル自身に分解させることができます。2番目のバージョンでは、/deepsearch コマンドでモデルにまずサブ問題をリストアップさせ、次に deep_search を個別に呼び出すように強制できます。
情報源の品質階層化
DeepSearch の出力は、検索 API のスコアだけで並べ替えるべきではありません。実際に技術記事を書く際には、私は以下の優先順位で情報源を確認します。
| 優先度 | 情報源 |
|---|---|
| P0 | 公式ドキュメント、ソースコード、リリースノート |
| P1 | 作者のブログ、メンテナーの説明、issue / PR |
| P2 | 高品質なチュートリアル、技術分析 |
| P3 | コミュニティディスカッション、Reddit、X、フォーラム |
Extension は formatResultsForModel() 内で情報源の種類を事前にマークできます。
function classifySource(url: string): "official" | "source" | "community" | "other" {
const host = new URL(url).hostname;
if (host === "pi.dev") return "official";
if (host === "github.com") return "source";
if (host.includes("reddit.com")) return "community";
return "other";
}これにより、モデルが総合する際に、コミュニティの噂と公式ドキュメントを同じ証拠レベルで扱うことがなくなります。
コンテキストの切り詰め
検索結果はコンテキストを簡単に汚染します。DeepSearch のツール出力は少なく、かつ洗練されているべきです。
私の提案は次のとおりです。
| 内容 | ツール出力に含めるか |
|---|---|
| タイトル | 含める |
| URL | 含める |
| 200-500 字の要約 | 含める |
| ページ全文 | デフォルトでは含めない |
| 元の HTML | 含めない |
| 検索 API の元の JSON | details に含めるが、本文には含めない |
全文を読む必要がある場合は、別のツールを作成できます。
fetch_source(url)これにより、DeepSearch の最初のステップでは候補となる情報源を見つけ、2番目のステップでは最も重要な2〜3ページのみを取得します。最初から十数ページのウェブページ全体をモデルに渡すべきではありません。
よくある質問
なぜ直接 bash で検索スクリプトを実行しないのですか?
可能です。しかし、extension の方が安定しています。
bash を使用する際の問題は、モデルが毎回コマンド、パラメータ、出力形式、エラー処理を再決定する必要があることです。Extension はこれらの詳細を固定するため、モデルは deep_search を呼び出すだけで済みます。
なぜ要約も extension に書かないのですか?
最初のバージョンではお勧めしません。
extension 自身が別のモデルを呼び出して要約を行う場合、ネストされたモデル呼び出し、コスト計算、コンテキストのずれ、引用責任の問題に直面します。より簡単な方法は、extension が証拠のみを返し、Pi の現在のセッション内のモデルが総合を担当することです。
この DeepSearch は MCP と見なされますか?
いいえ、違います。これは Pi extension が登録したローカルツールです。
もし成熟した MCP 検索サーバーをすでに持っている場合、Pi の MCP 関連パッケージや extension を通じて接続することもできます。しかし、このケースでは Pi 自体の拡張メカニズムを理解するために、直接 extension を書くことを選択しました。
セキュリティに関して注意すべきことは何ですか?
少なくとも4つのことに注意してください。
| リスク | 対策 |
|---|---|
| API キーの漏洩 | 環境変数からのみ読み込み、リポジトリに書き込まない |
| 信頼できないウェブコンテンツ | ウェブコンテンツをシステム命令として扱わず、検証すべき証拠としてのみ扱う |
| 検索結果の汚染 | 公式情報源とソースコードを優先し、コミュニティ結果の重みを下げる |
| コンテキストの爆発 | 結果の数と要約の長さを制限する |
DeepSearch は「検索強化」のように見えますが、本質的には外部ウェブページをエージェントのコンテキストに取り込むことです。外部コンテンツがコンテキストに入ると、プロンプトインジェクションを現実的なリスクとして扱う必要があります。
まとめ
Pi Agent の最初の実践ケースを DeepSearch Extension としたのは、Pi の3つの重要な特徴を同時に示すことができるからです。
- Pi のコアは非常に小さく、すべてのワークフローを内蔵していません。
- 本当に役立つ機能は extension を通じて追加できます。
- Extension は単にコマンドを追加するだけでなく、モデルが外部世界にアクセスする境界を定義することでもあります。
このケースが動作すれば、Pi は単なるローカルコード編集エージェントではなく、制御可能な研究の入り口を持つことになります。外部資料が必要な問題に遭遇した場合、まず検索し、次にフィルタリングし、情報源を付けて回答することができます。
これは、モデルが記憶に基づいて回答するよりも信頼性が高く、毎回検索コマンドを手書きするよりも再利用性が高いです。
参考資料
Pi Extensions ドキュメント
公式拡張ドキュメント。TypeScript を使用してツール、コマンド、UI、イベントフックを登録する方法を紹介します。
Pi 使用ドキュメント
公式使用ドキュメント。インタラクティブモード、スラッシュコマンド、セッション、コンテキストファイル、CLIオプション、設計原則を網羅しています。