第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/sdk=SDK)を入れます。
npm install @anthropic-ai/sdk
1行ずつ読むと:
npm install:必要な部品(ライブラリ)をこのプロジェクトにダウンロードして追加するコマンド。@anthropic-ai/sdk:Anthropic公式の道具箱。これがあると、面倒な通信の細かい部分を全部やってくれて、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は毎回まっさら(ステートレス)。会話を続けたいなら、
userとassistantの発言を順番にmessagesへ積み続け、毎回まるごと渡す。この“履歴を積む”という地味な作業が、このあとエージェントの往復(道具の結果も履歴に積む)にそのまま化けます。
④ 動かす
ビルド(コンパイル)の準備なしで、TypeScriptをそのまま実行できる tsx で動かします。
npx tsx agent.ts
1行ずつ読むと:
npx:その場で必要なツールを取ってきて実行するコマンド(事前インストール不要)。tsx agent.ts:agent.ts(TypeScript)をそのまま実行します。>が出たら何か打ってみてください。返事が返り、「さっきの〜」と続けると会話がつながれば成功です(ts-nodeでも動きます)。
⚠️ ハマりどころ
- 鍵をコードに直書きする/gitに上げてしまう
→
new Anthropic({ apiKey: "sk-ant-..." })のようにコードへ直接貼らない。直書きすると、その状態でうっかりコミットして鍵が世界に公開されがちです。.envに入れて.gitignoreする——これを最初にやること。万一公開してしまったら、Anthropic の管理画面でその鍵をすぐ無効化(revoke)して作り直します。 - 履歴を渡さず「覚えてない」になる
→
messagesに積まずに毎回userInputだけ送ると、AIは毎回初対面。「さっきの話」がまったく通じません。userとassistantの両方を積むのが正解。とくに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配列に積んで毎回渡す(userとassistantの両方を積み、何往復でも会話が続くようにする)。 ③ モデルは定数const MODEL = "claude-opus-4-8";にする。 ④readlineでループ入力にして、exitで終了。temperatureなどの追加パラメータは入れないで。コードは短く、各行に日本語コメントを付けて。」
出てきたコードを見るときの確認ポイント:
- 鍵が
.envから読まれているか?(コードにsk-ant-...が直書きされていない/.gitignoreに.envがある) - 履歴を毎回渡しているか?(
messagesにuserとassistantの両方を積んでいる。会話が続く作りになっている) - 最初のメッセージが
userから始まっているか?(順番がuser → assistant → user …になっている) max_tokensが極端に小さくないか?(返事が途中で切れない)
📝 ことばメモ
- SDK(えすでぃーけー):ある機能を使うための公式の道具箱(ライブラリ)。ここでは
@anthropic-ai/sdk。これがあると、LLMへの電話を短い1行で書ける - 環境変数(かんきょうへんすう)/
.env:OSやプロセスが覚えておく「名前=値」のメモ。秘密の鍵などをコードの外に置くために使う。.envはその値をまとめて書くファイル(.gitignoreで必ず隠す) messages(めっせーじず)/role(ろーる):会話の履歴の配列。各発言にはrole(user=人間、assistant=AI) と中身が入る。LLMはこれを毎回まるごと読んで考えるstop_reason(すとっぷ・りーずん):LLMがなぜ返事を止めたかの理由。ふつうの会話では"end_turn"(言い切った)。次章で道具を使うときは"tool_use"(道具を使いたい)が登場します- ステートレス(毎回まっさら):LLMは前のやりとりを覚えていない性質。会話を続けるには、履歴を毎回こちらから渡す必要がある(前作ChatGPTクローン第4章と同じ話)
➡️ 次の章へ
これで、LLMと往復するだけのCLIができました。鍵は .env に隠し、履歴を毎回渡して会話を続ける——エージェントの土台です。
でも、今のCLIには 道具がまだ1つもありません。だから「test.js を読んで」と頼んでも、LLMは読むふりをするだけで、実際にはファイルを開けません(手が無い天才シェフのまま)。
次の第3章では、いよいよLLMに 「道具メニュー」を渡して、道具を使いたいと言わせる ところを作ります。ここからCLIが“ただのチャット”を抜け出し、エージェントへ一歩近づきます。