第5章 道具をつくる① ファイルを読む・一覧する

📖 この章のゴール:本物の道具として read_file(ファイルを読む)と list_files(フォルダの中身を一覧する)の2つを作り、エージェントが自分でプロジェクトを調べられるようになる。読み取りは「壊さない」ので比較的安全——でも「どこまで読めてしまうのか?」という宿題が、ここでこっそり生まれます。 ← 目次・はじめにへもどる


📱 Claude Code ではこう見える

ターミナルで Claude Code に「このバグ直して」とだけ頼んでみると、いきなり修正案を出すのではなく、まずこんな動きをします。

  1. 「まず関係しそうなファイルを見ます」と言って、フォルダの中身を一覧するsrc/ には何が入ってる?)
  2. あたりを付けて、いくつかのファイルを勝手に開いて読むindex.ts の中身は…)
  3. 中身を読んで「ここが原因ですね」と把握してから、ようやく直しに入る

あなたは「直して」としか言っていないのに、Claude Code は頼まれてもいないのに、自分でプロジェクトを調べ始めます。これができるのは、Claude Code が「フォルダを一覧する道具」と「ファイルを読む道具」を持っていて、それを必要だと思ったタイミングで自分から使うからです。

第3章では「今の時刻を返す」だけの、いわばおもちゃの道具を作りました。この章で作るのは、エージェントが「まず状況を調べる」ために本当に使う、最初の実用的な道具です。前章までで「考える↔動く」のループ(背骨🔁)はできています。あとはそのループに良い道具を差し込むだけで、エージェントは賢く振る舞い始めます。


🤔 なぜ「読む」から始めるのか(🟢 基礎)

道具には「読む(調べる)」系と「書く・実行する(変える)」系があります。この章であえて読む系から始めるのには理由があります。

読み取りは「壊さない」ので、比較的安全だからです。

ファイルを読むだけなら、中身が消えたり書き換わったりはしません。コマンドを走らせるわけでもない。だから万一エージェントが変な動きをしても、最悪「見当ちがいのファイルを覗いた」くらいで、取り返しはつきます。道具づくりの最初の一歩にちょうどいいのです。

次章(第6章)で作る「書く」道具は、ここから一気に話が変わります。書く=上書きなので、大事なファイルを壊せてしまう。だから本書は、まず安全な読みで道具の作り方そのものに慣れ、危なさは1つずつ足していきます。

でも「比較的」安全、なのがミソ(🔧 応用・伏線)

「読み取りは安全」と言いましたが、「比較的」です。よく考えると、こわい穴が一つ空いています。

あなたは「このプロジェクトのファイルを読んでいい」と思って道具を渡します。 でも、もしエージェントが read_file("../../.ssh/id_rsa") のように、作業フォルダの外を読もうとしたら?

..(ひとつ上のフォルダへ、の意味)をいくつも重ねれば、パソコンのどこのファイルでも読めてしまう——いまの素朴な作りでは、それを止めるものがありません。読み取りですら、放っておくと「プロジェクトの外の秘密ファイルまで覗ける道具」になりうるのです。

これは本書を貫く伏線です。この穴は第9章(サンドボックスと最小権限)でしっかり封じます。いまは「道具には“どこまで触っていいか”の境界が要る」と頭の隅に置いておけば十分。まずは道具を動かすことに集中しましょう。

💡 ここが背骨🔁の効きどころです。道具を1つ足すたびに、エージェントの能力が1段増えます。 時刻しか分からなかったエージェントが、read_file を持てば「ファイルの中身を踏まえて答える」ようになり、list_files を持てば「どんなファイルがあるかを自分で調べる」ようになる。何ができるエージェントになるかは、あなたがどんな道具を渡すかで決まるのです。


🛠 こう作る(🟢 基礎)

部品は3つです。前章までのコードに足していくだけで、新しい仕組みはほとんど要りません。

  1. ファイル操作の標準ライブラリ fs/promises を読み込む
  2. tools(道具メニュー)に read_filelist_files定義を足す
  3. executeTool(実際に動かす係)に、その2つの中身を足す

① まずは fs/promises を読み込む

Node.js には、ファイルを読み書きするための機能が最初から入っています。その「待てる版(async/await で書ける版)」が fs/promises(えふえす・プロミセズ)です。

import * as fs from "node:fs/promises";

1行ずつ読むと:

  • import * as fs from "node:fs/promises"; … Node.js 標準のファイル操作機能を、まるごと fs という名前で読み込みます。node: を頭に付けるのは「これは自分で入れたパッケージではなく、Node.js に最初から入っているもの」という目印です(yarn add などのインストールは不要)。これ以降 fs.readFile(...)fs.readdir(...) のように使えます。

② 道具メニュー(tools)に2つ足す

第3章で tools の形は確立しました。LLMに「こんな道具が使えるよ」と知らせるためのメニュー(説明書き)です。そこへ2つ追加します。

const tools: Anthropic.Tool[] = [
  {
    name: "read_file",
    description: "指定したパスのファイルの中身を、文字列で返して読む。",
    input_schema: {
      type: "object",
      properties: {
        path: { type: "string", description: "読みたいファイルのパス。例: src/index.ts" },
      },
      required: ["path"],
    },
  },
  {
    name: "list_files",
    description: "指定したフォルダの中にあるファイル・フォルダの名前を一覧する。",
    input_schema: {
      type: "object",
      properties: {
        dir: { type: "string", description: "中身を見たいフォルダのパス。例: src" },
      },
      required: ["dir"],
    },
  },
];

1行ずつ読むと:

  • const tools: Anthropic.Tool[] = [ … 道具メニューの配列です。中に道具の定義を並べます(第3章と同じ形)。
  • name: "read_file", … 道具の名前。あとで「LLMがこの名前の道具を呼んだら…」とつなぐ目印になるので、ここの綴りは正確に
  • description: "...読む。", … この道具が何をするかの説明。LLMはこの説明文を読んで「いま使うべきか」を判断します。人に説明するつもりで書くのがコツ。
  • input_schema: { ... } … この道具にどんな引数を渡すかの決まり。「object(かたまり)の形で渡してね」という宣言です。
  • properties: { path: { type: "string", ... } } … 引数の中身。read_filepath(読みたいファイルの場所)を文字列で1つ受け取ります。
  • required: ["path"],path必ず要る引数だ、という指定。これがあると、LLMはパスを省略せずに渡してくれます。
  • 下半分の list_files も同じ形で、受け取る引数が path ではなく dir(フォルダの場所)になっただけです。

📝 ここはまだ「メニューに載せただけ」。注文を受けて実際に手を動かす部分は、次の executeTool で作ります。メニューに料理名を書いても、厨房が作れなければ出てこないのと同じです。

③ 実際に動かす係(executeTool)に中身を足す

LLMが「read_file を使いたい、pathREADME.md」と言ってきたとき、本当にファイルを読むのがこの関数です。第4章で作った executeToolswitch に、case を2つ足します。

async function executeTool(name: string, input: any): Promise<string> {
  switch (name) {
    case "read_file":
      try {
        return await fs.readFile(input.path, "utf-8");
      } catch (e) {
        return `読み込み失敗: ${(e as Error).message}`;
      }

    case "list_files":
      try {
        const names = await fs.readdir(input.dir);
        return names.join("\n");
      } catch (e) {
        return `一覧の取得に失敗: ${(e as Error).message}`;
      }

    default:
      return `不明な道具: ${name}`;
  }
}

1行ずつ読むと:

  • async function executeTool(name: string, input: any): Promise<string> { … 「道具の名前」と「引数」を受け取り、結果を文字列で返す係です。async なのは、ファイル読み込みのように「少し待つ」作業を await で待つため。返り値が必ず文字列なのは、結果をそのままLLMに渡すから(LLMは文字列を読んで考える)。
  • switch (name) { … 渡された道具の名前で、行き先を振り分けます。
  • case "read_file": … 名前が read_file のときの処理。さっきメニューに書いた name同じ綴りでつないでいます。
  • try { … ここから失敗するかもしれない処理。読み込みは「ファイルが無い」などで失敗しうるので、必ず try/catch で囲みます。
  • return await fs.readFile(input.path, "utf-8");この章の主役の1行input.path(LLMが指定したファイルの場所)を開き、中身を読んで返します。第2引数の "utf-8" は「人が読める文字列として読んで」という指定(これを忘れると、文字ではなく生のデータの塊で返ってきてしまいます)。
  • } catch (e) { … もし読み込みが失敗したら、ここに来ます。
  • return \読み込み失敗: ${(e as Error).message}`;` … 失敗したことを、エラーで止まるのではなく“結果の文字列”として返します。なぜそうするかは後で詳しく説明しますが、ひとことで言うと「LLMに『その道具は失敗したよ』と伝えて、別の手を考えさせる」ためです。
  • case "list_files": … 名前が list_files のときの処理。
  • const names = await fs.readdir(input.dir);input.dir(指定フォルダ)の中にある名前の一覧を、配列として受け取ります(例:["index.ts", "utils.ts", "data"])。
  • return names.join("\n"); … 配列のままではLLMに渡しにくいので、join("\n")1行に1つずつ並べた文字列に変換して返します。\n は「改行」のこと。
  • default: … メニューに無い未知の名前が来たときの保険。第4章から引き継いだ1行です。

これで完成です。新しい関数も、新しいループも要りません。前章のループに、道具の“中身”を2つ足しただけ。これがエージェント開発の気持ちよさで、骨組み(ループ)が一度できれば、能力は道具を足すだけで増えていきます

デモ:「README を読んで要約して」

道具がつながったか、実際に確かめましょう。agent.ts のあるフォルダに、ためしに README.md を1つ置いてから(中身は数行の説明文で十分)、起動します。

npx tsx agent.ts

1行ずつ読むと:

  • npx tsx agent.tsagent.ts をそのまま動かすコマンド(第2章で確立した起動方法)。> の入力待ちが出たら準備OKです。

入力待ちに、こう打ちます。

> README.md を読んで、3行で要約して

すると、エージェントは内部でこんな往復をします(背骨🔁が動いている瞬間です)。

  1. LLM:「中身を見ないと要約できない。read_file を使う、pathREADME.md
  2. あなたのコード(executeTool):実際に README.md を読み、中身の文字列をLLMに返す
  3. LLM:返ってきた中身を踏まえて「この3行が要約です」と答えて終了

ポイントは、あなたは「読んで」と一度言っただけなのに、エージェントが「読む→中身を踏まえて答える」の2手を自分で踏んでいること。list_files も同じように「src の中に何がある?」と聞けば、フォルダの一覧を自分で取ってから答えてくれます。道具を渡したぶんだけ、エージェントは自分で調べられるようになったわけです。


⚠️ ハマりどころ

  • パスを鵜呑みにすると、作業フォルダの外まで読めてしまう(🔧 伏線) いまの read_file は、LLMが言ってきた input.pathそのまま開きます../ を重ねたパスを渡されれば、プロジェクトの外(他人の秘密ファイルなど)まで読めてしまう。いまは穴が空いたままで構いません——ただし「ここは後で塞ぐ」と覚えておくこと。封じ込めは第9章でやります。

  • 巨大ファイルを読むと、トークンが爆発する(🔧 応用) read_file中身を丸ごと返します。もし数万行のログファイルを読ませると、その中身がまるごとコンテキスト(AIが一度に見ている情報)に積まれ、料金(トークン)も跳ね上がり、上限を超えてエラーになることも。大きなファイルは「最初のN行だけ」「特定の語を含む行だけ」のように読む量を絞る工夫が要ります(コストの考え方は付録F)。いまは小さなファイルで試せばOK。

  • 存在しないパスでエージェントを“落とさない” read_file("nai.txt") のように無いファイルを指定すると、fs.readFileエラーを投げますtry/catch で囲んでいないと、プログラムそのものが止まってしまう。だから上のコードでは囲んで、「読み込み失敗: …」という“結果”として返すようにしました。こうすると、LLMは「あ、無いのか。じゃあ list_files で名前を確かめよう」と自分で立て直せます。エラーは「止める理由」ではなく「LLMに渡す手がかり」なのです。

  • メニューと中身で名前がズレる toolsname"read_file")と、executeToolcase "read_file"綴りが1文字でも違うと、その道具は呼ばれても default(不明な道具)に落ちます。名前は必ず一致させましょう。


🤖 AIに頼むなら(Vibe codingのコツ)

道具を1つ足すこの種の作業は、AIエージェント(Claude Code など)に頼むと速いです。ただし丸投げすると、try/catch を省いたり、パスをそのまま開く危ない版が出てきがち。何を守ってほしいかを明示して頼みましょう。

🗣 プロンプト例: 「いま tools 配列と executeTool 関数があるTypeScriptのCLIエージェントに、read_filelist_files の2つの道具を足してほしい。条件は4つ:(1) fs/promises を使う。(2) read_filepath を、list_filesdir を引数に取る形で、tools の定義executeToolcase の両方を、同じ名前で足す。(3) ファイル読み込みやフォルダ一覧は try/catch で囲み、失敗時は例外を投げずに“失敗した旨の文字列”を返す(LLMに結果として渡したいので)。(4) いまはパスの検証(作業フォルダの外を弾く処理)は入れなくていい——それは後の章でまとめてやる、と分かるようにコメントだけ残して。」

出てきたコードを見るときの確認ポイント:

  • toolsnameexecuteToolcase綴りが一致しているか?
  • ファイル操作が try/catch で囲まれ、失敗を“結果の文字列”として返しているか?(エラーで落ちない)
  • パスの検証は「後章でやる」前提になっているか?(いまここで中途半端な検証を足して、かえって分かりにくくしていないか)
  • 大きすぎる出力に注意——丸ごと読む実装であることを理解しているか?(巨大ファイルでトークンが膨らむ)
  • read_file"utf-8" を渡し、人が読める文字列で返しているか?

📝 ことばメモ

  • fs/promises(えふえす・プロミセズ):Node.js 標準の「ファイル操作」機能の、async/await で待てる版。fs.readFile(読む)・fs.readdir(フォルダの中身を一覧)などが入っている。node:fs/promises と書いて読み込む(インストール不要)
  • read_file(りーど・ふぁいる):この章で作った道具。path(ファイルの場所)を受け取り、await fs.readFile(path, "utf-8")中身を文字列にして返す。読み取りなので比較的安全
  • list_files(りすと・ふぁいるず):この章で作った道具。dir(フォルダの場所)を受け取り、await fs.readdir(dir)中の名前の一覧を取り、join("\n") で1行ずつの文字列にして返す
  • パス封じ込め(伏線):道具が触れる範囲を「作業フォルダの中だけ」に制限する考え方。../ で外へ出るのを防ぐ。いまの素朴な実装にはまだ無い=穴が空いた状態。第9章でしっかり塞ぐ
  • 「エラーを結果として返す」:道具が失敗しても例外で止めず、「失敗した」という文字列をLLMに渡す作法。LLMが状況を理解して別の手を考えられるようになる

➡️ 次の章へ

おめでとうございます。エージェントが自分でプロジェクトを“読んで調べる”ようになりました。でも、できるのはまだ「見る」だけ。バグを見つけても、自分では直せません

次の第6章では、いよいよ「書く」道具(write_file)を作ります。読めるだけだったエージェントが、ファイルを書き換えられるようになる——とても便利です。が、ここで初めて「上書き=取り返しがつかないかも」という、本物の危なさが顔を出します。本書で「危ない道具」が登場するのは、ここからです。

次の章:道具をつくる② ファイルを書く・直す(はじめての危険) →

← 目次・はじめにへもどる