第2章 まずLLMと往復するだけのCLIを作る

📖 この章のゴール:ターミナルでLLMと会話するだけの、いちばん小さなCLIを自分の手で作る。あわせて APIキー(秘密の鍵)を自分のPCに安全に隠す やり方と、「会話が続くのは、毎回これまでの履歴をぜんぶ渡しているからだ」(=ステートレス) を腹落ちさせる。道具はまだ無し。でもこれが、これから作るエージェントの土台になります。 ← 目次・はじめにへもどる


📱 Claude Code ではこう見える

Claude Code を起動して、プロンプト(> の入力欄)に何か打つと、返事が返ってきます。

> こんにちは
こんにちは!今日は何をお手伝いしましょうか?
> さっきの「こんにちは」って何語?
日本語ですね。

注目してほしいのは2回目です。「さっきの」と言うだけで、ちゃんと前のやりとりを覚えている。会話がつながっています。

この章では、まさにこの 「打つと返事が返る」「会話がつながる」 という部分だけを、自分のCLIとして作ります。道具(ファイルを読む・コマンドを走らせる…)はまだ付けません。まずはLLMと“おしゃべりするだけ”の最小の往復を作り、その上に次章から道具を載せていきます。

💡 「ただのチャットなら前作(ChatGPTクローン)でやったのでは?」と思うかもしれません。その通りです。今回はそれを ターミナルだけ・ブラウザもサーバーも無しで 作り直します。エージェントの“心臓”になる往復を、いちばん裸の形で確かめるためです。


🤔 なぜ自分のCLIを作る?(🟢 基礎)

「Claude Code があるのに、なぜ自分で作るの?」——中身を部品レベルで理解するためです。自分で1から組むと、「魔法」に見えていたものが「ただの往復」だと分かります。

まず、いちばん大事な前提を思い出します(第1章の復習)。

🍳 LLM=電話の向こうの天才シェフ。あなたのCLIは、その電話をかける係です。 LLMはあなたのPCの中にはいません。Anthropic(Claude を作っている会社)のサーバーにいます。だからあなたのCLIは、API(えーぴーあい)越しに「これ考えて」と電話をかけ、答えをもらうだけ。頭脳そのものは借りものです。

そしてもうひとつ、初心者がいちばん引っかかるところを先に潰しておきます。

会話が続くのは「覚えているから」ではない(🟢 基礎)

さっきの Claude Code は「さっきの」を覚えていました。でも実は——

LLMは、前のやりとりを何も覚えていません。 毎回、まっさら(白紙)の状態で電話に出ます。これを ステートレス(毎回まっさら) と言います。

ではなぜ会話がつながって見えるのか。タネは単純です。

あなたのCLIが、これまでの会話を全部いっしょに毎回渡しているから。

シェフ(LLM)は記憶喪失で、毎回「はじめまして」の状態。だからあなたが電話するたびに、これまでの会話の台本を頭から全部読み上げてから、新しい質問をするのです。

1回目の電話:
  あなた→シェフ:「『こんにちは』に返事して」
  シェフ→あなた:「こんにちは!」

2回目の電話(シェフは1回目を忘れている):
  あなた→シェフ:「さっきこういう会話をした↓
                  ・あなた:こんにちは
                  ・あなた(シェフ役):こんにちは!
                  …で、さっきの『こんにちは』って何語?」
  シェフ→あなた:「日本語ですね」

2回目に台本(履歴)を渡しているから、シェフは「さっき」が分かる。渡さなければ、「さっきって何のこと?」になります。

🍱 これは前作の復習です。ChatGPTクローンの第4章でも、「会話の記憶はサーバー側に保存しておいて、毎回まとめてLLMに渡す」というのをやりました。仕組みはまったく同じ。今回はその“履歴を毎回渡す”を、自分のCLIの中で素朴に再現します。

なぜこの話を最初にするか。エージェントの往復(指示→実行→結果→また指示…)も、結局これと同じだからです。LLMは毎回まっさら。だから「これまでに何をしたか」を、毎回こちらが履歴として渡し続ける。この感覚が、この章でいちばん腹落ちしてほしいことです。

鍵(APIキー)は自分のPCに隠す(🟢 基礎)

電話をかけるには、契約者だと証明する秘密の番号が要ります。それが APIキーANTHROPIC_API_KEY)。これはパスワードと同じで、他人に知られると勝手にあなたの名義で電話をかけられ、料金を請求されます

💡 前作でも「APIキーをフロント(ブラウザ側)に出さない」を強く言いました。今回はブラウザもサーバーも無いので、もっとシンプルです。鍵は自分のPCの中(環境変数)にだけ置く。やり方はこのあとすぐ作ります。


🛠 こう作る(🟢 基礎)

作るものは3ステップだけです。①SDKを入れる → ②鍵を隠す → ③CLI本体を書く。最後に動かします。

① SDK(公式の道具箱)を入れる

LLMに電話するための公式ライブラリ(@anthropic-ai/sdkSDK)を入れます。

npm install @anthropic-ai/sdk

1行ずつ読むと:

  • npm install:必要な部品(ライブラリ)をこのプロジェクトにダウンロードして追加するコマンド。
  • @anthropic-ai/sdkAnthropic公式の道具箱。これがあると、面倒な通信の細かい部分を全部やってくれて、anthropic.messages.create(...) のような短い1行でLLMに電話できます。自分で通信を書く必要はありません。

② 鍵を .env に隠して、gitに上げない

プロジェクトのフォルダに .env(どっと・いーえぬぶい)という名前のファイルを作り、鍵を1行だけ書きます。

# .env というファイルの中身(このファイルは絶対に外に出さない)
ANTHROPIC_API_KEY=sk-ant-ここにあなたの鍵を貼る

1行ずつ読むと:

  • ANTHROPIC_API_KEY=...環境変数(OSが覚えておく「名前=値」のメモ)の形で鍵を書いています。新しいAnthropic() は、この名前の環境変数を自動でさがして使います。だからコードに鍵を直接書かなくて済みます。
  • sk-ant-...:あなた専用の鍵。取り方は付録Aを見てください(Anthropic のサイトで発行します)。

次に、この鍵を間違ってGitHubに公開しないための“ふた”をします。.gitignore(git・いぐのあ)というファイルに .env を1行足します。

# .gitignore というファイルの中身
.env

1行ずつ読むと:

  • .gitignore は git に「この名前のファイルは記録(コミット)するな」と伝えるリストです。
  • .env と書いておくと、git が .env最初から見えないもの扱いにします。これで「うっかり git add して鍵を世界中に公開」といういちばん多い事故を防げます。

⚠️ ここがセキュリティの肝です。鍵をコードに直書きしない/.env に入れて .gitignore する。この2つだけ守れば、初心者がやりがちな鍵流出はほぼ防げます。

.env を読み込むために、小さな部品をもう1つ入れておきます。

npm install dotenv

1行ずつ読むと:

  • dotenv.env ファイルを読んで、中身を環境変数としてプログラムに教えてくれる道具。これを最初に1回呼ぶだけで、ANTHROPIC_API_KEY がプログラムから見えるようになります。

③ CLI本体 agent.ts を書く

いよいよ本体です。agent.ts というファイルを作り、まず1往復だけ(打って → 返事が返る)を作ってから、会話が続く形へ育てます。完成形を先に置き、下で1行ずつ読みます。

import "dotenv/config"; // .env を読み込んで環境変数にする(いちばん上で1回だけ)
import Anthropic from "@anthropic-ai/sdk";
import * as readline from "node:readline/promises";

const anthropic = new Anthropic(); // 環境変数 ANTHROPIC_API_KEY を自動で読む
const MODEL = "claude-opus-4-8"; // 賢い既定。コストは付録Fで安いモデルに替えられる

const SYSTEM_PROMPT = "あなたは親切なアシスタントです。日本語で簡潔に答えてください。";

const messages: Anthropic.MessageParam[] = []; // ← 会話の履歴(最初は空っぽ)

const rl = readline.createInterface({ input: process.stdin, output: process.stdout });

while (true) {
  const userInput = await rl.question("> ");
  if (userInput === "exit") break;

  messages.push({ role: "user", content: userInput }); // あなたの発言を履歴に積む

  const res = await anthropic.messages.create({
    model: MODEL,
    max_tokens: 1024,
    system: SYSTEM_PROMPT,
    messages, // ← これまでの履歴を毎回まるごと渡す
  });

  const reply = res.content
    .filter((block) => block.type === "text")
    .map((block) => block.text)
    .join("");

  console.log(reply);

  messages.push({ role: "assistant", content: res.content }); // AIの返事も履歴に積む
}

rl.close();

1行ずつ読むと:

  • import "dotenv/config";いちばん最初.env を読み込み、鍵を環境変数にセットします。これより後で new Anthropic() を呼ぶので、順番が大事です。
  • import Anthropic from "@anthropic-ai/sdk";:さっき入れた公式の道具箱を読み込みます。
  • import * as readline from "node:readline/promises";ターミナルから1行ずつ入力を読むための、Node.js標準の部品です(promises 版だと await で待てて読みやすい)。
  • const anthropic = new Anthropic();:LLMに電話する受話器を用意。鍵は環境変数から自動で読むので、ここに鍵を書く必要はありません(だから安全)。
  • const MODEL = "claude-opus-4-8";:使うモデル(頭脳の種類)を定数にします。賢い既定。コストが気になれば、ここ1か所を安いモデルに替えるだけ(やり方は付録F)。
  • const SYSTEM_PROMPT = "...";:頭脳に最初に渡しておく「性格・やり方」の指示(第1章で出てきたあれ)。いまは1文だけ。深掘りは第10章。
  • const messages: Anthropic.MessageParam[] = [];会話の履歴を入れる箱。MessageParam は「1つの発言(誰が・何を言ったか)」の型。最初は空っぽです。
  • readline.createInterface(...):ターミナルの入力(キーボード)と出力(画面)をつなぐ準備です。
  • while (true) { ... }ずっとくり返す入力ループ。打つたびに1回まわります。
  • const userInput = await rl.question("> ");> と表示して、あなたが1行打つのを待ちます
  • if (userInput === "exit") break;exit と打ったらループを抜けて終了します。
  • messages.push({ role: "user", content: userInput });:あなたの発言を role: "user"(人間の発言) として履歴に積みます。これがステートレス対策の本体です。
  • await anthropic.messages.create({ ... }):ここが電話本体model(頭脳)・max_tokens(返事の最大の長さ)・system(性格)・messages(履歴)を渡して、返事をもらいます。毎回 messages を丸ごと渡しているのが最重要ポイント。
  • res.content.filter(...).map(...).join(""):返事はブロックの配列で返ります。その中からテキストのブロック(block.type === "text")だけを取り出し、.text をつなげて1つの文字列にします(いまはテキストしか来ませんが、この形に慣れておくと次章で道具を扱うとき楽です)。
  • console.log(reply);:取り出した返事を画面に表示します。
  • messages.push({ role: "assistant", content: res.content });AIの返事も role: "assistant" として履歴に積みます。これを忘れると、次の質問でAIは「自分がさっき何と答えたか」を知らないままになります。だから多ターン(何往復も)の会話になるのです。
  • rl.close();:終了時に入力を閉じます。

🔁 ここが背骨です。LLMは毎回まっさら(ステートレス)。会話を続けたいなら、userassistant の発言を順番に messages へ積み続け、毎回まるごと渡す。この“履歴を積む”という地味な作業が、このあとエージェントの往復(道具の結果も履歴に積む)にそのまま化けます。

④ 動かす

ビルド(コンパイル)の準備なしで、TypeScriptをそのまま実行できる tsx で動かします。

npx tsx agent.ts

1行ずつ読むと:

  • npx:その場で必要なツールを取ってきて実行するコマンド(事前インストール不要)。
  • tsx agent.tsagent.ts(TypeScript)をそのまま実行します。> が出たら何か打ってみてください。返事が返り、「さっきの〜」と続けると会話がつながれば成功です(ts-node でも動きます)。

⚠️ ハマりどころ

  • 鍵をコードに直書きする/gitに上げてしまうnew Anthropic({ apiKey: "sk-ant-..." }) のようにコードへ直接貼らない。直書きすると、その状態でうっかりコミットして鍵が世界に公開されがちです。.env に入れて .gitignore する——これを最初にやること。万一公開してしまったら、Anthropic の管理画面でその鍵をすぐ無効化(revoke)して作り直します。
  • 履歴を渡さず「覚えてない」になるmessages に積まずに毎回 userInput だけ送ると、AIは毎回初対面。「さっきの話」がまったく通じません。userassistant の両方を積むのが正解。とくに assistant(AIの返事)の積み忘れは気づきにくいので注意。
  • max_tokens が小さすぎて返事が途中で切れるmax_tokens返事(出力)の最大の長さ。これを 50 などにすると、長い答えが途中でブツッと切れます。最小例では 1024 程度に。足りなければ増やします(増やすほど料金は上がる——付録F)。
  • 最初のメッセージは user から始める → 会話は必ず user(人間)→ assistant(AI)→ user → … の順。いきなり assistant から始めたり、user が2回続いたりするとエラーになります。上のコードのように「まず user を積んでから電話する」を守れば自然と正しい順番になります。
  • dotenv の読み込みが遅いimport "dotenv/config";ファイルのいちばん上に。これより後ろだと、鍵が読まれる前に new Anthropic() が走ってしまい「鍵が無い」エラーになります。

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

この章のCLIは短いので、AI(Claude Code など)に下書きさせるのにちょうどいいです。ただ「チャットCLI作って」だけだと、鍵を直書きしたり、履歴を渡さない(会話が続かない)コードが出てくることがあります。ポイントを指定して頼みましょう。

🗣 プロンプト例: 「TypeScript(Node.js, ESM)と @anthropic-ai/sdk で、ターミナルで動くチャットCLIを作って。条件は4つ。 ① APIキーは .env から読む(dotenv を使い、コードに直書きしない。.gitignore.env を足すことも書いて)。 ② 会話の履歴を messages 配列に積んで毎回渡すuserassistant の両方を積み、何往復でも会話が続くようにする)。 ③ モデルは定数 const MODEL = "claude-opus-4-8"; にする。 ④ readline でループ入力にして、exit で終了。 temperature などの追加パラメータは入れないで。コードは短く、各行に日本語コメントを付けて。」

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

  • 鍵が .env から読まれているか?(コードに sk-ant-... が直書きされていない/.gitignore.env がある)
  • 履歴を毎回渡しているか?(messagesuserassistant両方を積んでいる。会話が続く作りになっている)
  • 最初のメッセージが user から始まっているか?(順番が user → assistant → user … になっている)
  • max_tokens が極端に小さくないか?(返事が途中で切れない)

📝 ことばメモ

  • SDK(えすでぃーけー):ある機能を使うための公式の道具箱(ライブラリ)。ここでは @anthropic-ai/sdk。これがあると、LLMへの電話を短い1行で書ける
  • 環境変数(かんきょうへんすう)/.env:OSやプロセスが覚えておく「名前=値」のメモ。秘密の鍵などをコードの外に置くために使う。.env はその値をまとめて書くファイル(.gitignore で必ず隠す
  • messages(めっせーじず)/role(ろーる):会話の履歴の配列。各発言には roleuser=人間、assistant=AI) と中身が入る。LLMはこれを毎回まるごと読んで考える
  • stop_reason(すとっぷ・りーずん):LLMがなぜ返事を止めたかの理由。ふつうの会話では "end_turn"(言い切った)。次章で道具を使うときは "tool_use"(道具を使いたい)が登場します
  • ステートレス(毎回まっさら):LLMは前のやりとりを覚えていない性質。会話を続けるには、履歴を毎回こちらから渡す必要がある(前作ChatGPTクローン第4章と同じ話)

➡️ 次の章へ

これで、LLMと往復するだけのCLIができました。鍵は .env に隠し、履歴を毎回渡して会話を続ける——エージェントの土台です。

でも、今のCLIには 道具がまだ1つもありません。だから「test.js を読んで」と頼んでも、LLMは読むふりをするだけで、実際にはファイルを開けません(手が無い天才シェフのまま)。

次の第3章では、いよいよLLMに 「道具メニュー」を渡して、道具を使いたいと言わせる ところを作ります。ここからCLIが“ただのチャット”を抜け出し、エージェントへ一歩近づきます。

ツール使用 — AIに「道具メニュー」を渡す →

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