ハクソク

世界を動かす技術を、日本語で。

200行のコードで「Claude Code」をコーディングする方法

概要

  • AIコーディングアシスタントの本質は約200行のシンプルなPythonコード
  • LLMとツールボックスの連携による会話ベースの実行フロー
  • 必要なツールは「ファイル読み込み」「ファイル一覧取得」「ファイル編集」の3つ
  • 実装の流れと各ツールの詳細な説明
  • 実践例や本格的なツールとの違い、拡張のヒント

AIコーディングエージェントの仕組みと実装

  • AIコーディングアシスタントの本質は「魔法」ではなく、LLMとツールの連携による会話型エージェント
  • ユーザーが要件を入力し、LLMが必要なツール呼び出しを指示
  • プログラムがツールを実行し、結果をLLMへ返却
  • LLM自身はファイルシステムへ直接アクセスしない設計
  • このループがエージェントの全体構造

必須ツール3種

  • ファイル読み込み
    LLMがプロジェクトコードを確認するための機能

  • ファイル一覧取得
    ディレクトリ内ファイルを一覧表示し、プロジェクトをナビゲート

  • ファイル編集
    コードの生成・修正・新規作成を指示

    • Claude Code等の本格的なエージェントではgrepやbash、websearch等の追加機能も搭載

基本的なセットアップ

  • 必要なインポート(inspect, json, os, anthropic, dotenv, pathlib, typing)
  • APIクライアントの初期化(例:AnthropicのClaude利用)
  • ターミナル出力の色分けによる可読性向上
  • ファイルパス解決ユーティリティで絶対パスを取得

ツールの実装

  • 各ツール関数には詳細なdocstringを付与し、LLMが自律的にツール選択できるよう設計

    • read_file_tool
      ファイル名を受け取り、中身を返却する辞書を生成
    • list_files_tool
      ディレクトリ内のファイルと種類(ファイル/ディレクトリ)を返却
    • edit_file_tool
      old_strが空なら新規作成、それ以外はold_strをnew_strに置換
      • old_str未検出時の挙動も明確化

ツールレジストリとLLMへの知識伝達

  • TOOL_REGISTRYで関数名と実体を紐付け
  • get_tool_str_representationでdocstringとシグネチャを抽出し、LLMにツール一覧を提示
  • SYSTEM_PROMPTでツールの使い方・呼び出しフォーマットを指示
  • LLMは「tool: TOOL_NAME({JSON_ARGS})」形式でツール呼び出しを返却

ツール呼び出しのパース

  • extract_tool_invocationsでLLMの出力からツール呼び出しを抽出
  • 「tool: name({...})」行をパースし、関数名と引数辞書のリストを生成

LLM呼び出しラッパー

  • execute_llm_callで会話履歴とシステムプロンプトを渡し、LLMの応答を取得

エージェントループ全体像

  • 外側ループ:ユーザー入力を取得し、会話履歴に追加
  • 内側ループ:LLM応答を解析し、ツール呼び出しがあれば実行
    • ツール呼び出しがなければ応答を表示し、内側ループ終了
    • ツール実行結果を会話履歴に追加し、再度LLMへ
  • 複数ツールの連続呼び出しにも対応する構造

実行例

  • 新規ファイル作成指示:「hello.pyにHello Worldを実装して」
    • edit_fileツールで新規作成、LLMが完了報告
  • 複数ステップ:「hello.pyに掛け算関数を追加して」
    • read_fileで内容確認→edit_fileで関数追加

本格的なツールとの違い

  • 本実装は約200行のシンプル構成
  • Claude Code等の製品では
    • 例外処理やフォールバック
    • ストリーミング応答
    • 長大ファイルの要約等の文脈管理
    • 追加ツールや破壊的操作の承認フロー
  • しかし基本構造は同じで、LLMが判断し、コードが実行、結果を返却

拡張と学習のヒント

  • LLMプロバイダの差し替えやプロンプトの調整、ツール追加も容易
  • シンプルなパターンながら高い拡張性
  • AIソフトウェア開発の先端技術を学ぶ教材やコースも紹介

このパターンを理解し、拡張することで自作のAIエージェント開発が可能

Hackerたちの意見

この記事は1年前にはかなり真実に近かったけど、今はハーネスがシンプルなエージェントループを超えてるから、これがクロードコードの動きの正確なメンタルモデルとは言えないと思う。
興味あるな、それについて詳しく教えてくれない?
最近のバージョンでもすごく変わったみたいだね。前は頻繁に中断して指示しなきゃいけなかったことを、今はどうやってやってるのかもっと知りたいな。
この記事は2025年1月に公開されたんだね。(タイトルに2025年って入れるべきだった?時間が経つの早いな。)
でも、その余計な複雑さって実際にパフォーマンスを向上させるの? https://www.tbench.ai/leaderboard/terminal-bench/2.0 ではそうだと言ってるけど、思ったほどじゃないみたい。「ターミナス」って基本的にはtmuxセッションとLLMのループだよ。
もちろん現代のハーネスはより良い機能を持ってるけど、それがメンタルモデルを無効にするとは思わないな。基盤のモデルが同じなら、シンプルなエージェントもパフォーマンスではそんなに遅れを取ってないよ。基本的なツールを使ったミニマルなものも含めてね。「自分でリレーショナルDBを作る」みたいな記事が基本的なBツリーとマージ結合を紹介するのと似てる。実際のエンジンには洗練されたプランナーや複数の結合方法、ブルームフィルターとかがあるけど、基盤のメンタルモデルはまだ正確だよ。
あなたが思っているほど真実ではないよ。昨年の進展の多くは、エージェントのプロンプトやツールを洗練させて、モデルが自由に動けるようにすることに関係している。サブエージェントやMCP、スキルはどれもまあまあで、ツールの出力を永遠に持ち運ばないようにするためのコンテキストの最適化もあったけど、それは主に長時間動作するエージェントにとってのメリットで、短いタスクではあまり気づかないよ。
同意。codex-cliリポジトリを使って、エージェントにコア機能を分析してもらうと、より良いモデルが得られるよ。
もう少し詳しいことがあるよ!例えば、投稿のエージェントは「早期停止」を示すことがあって、タスクが本当に終わる前に終了しちゃうんだ。これを推論モデルで解決できると思うかもしれないけど、実際にはSOTAモデルではうまくいかない。早期停止を修正するには、エージェントハーネスに追加機能が必要なんだ。クロードコードはTODOを使って、残っているタスクをLLMに思い出させるために、すべてのプロンプトに注入してるよ。(もし興味があれば、HolmesGPTの公開リポジトリのどこかに、これを解決するために行った実験のベンチマークがあるよ。仮説追跡から他のエキゾチックなアプローチまで、でもTODOがいつも一番良い結果を出してた。)それでも、いい記事だね。エージェントは本当にループの中のツールに過ぎない。難しいことじゃないよ。
なんで早く止まるの?例を教えて。
いいポイントだね、みんな知っておくべきだと思う。コーディングエージェントの核心は本当にシンプルで、ツール呼び出しのループなんだ。そうは言っても、こんな記事を書いて「クロードコードを200行でコーディングする方法」とか名付けるなら、少なくとも2025年4月に出たThorsten Ballの素晴らしい記事「エージェントの作り方、または: 皇帝は裸だ」というのを参考にすべきだよ(https://ampcode.com/how-to-build-an-agent)!これは(私の知る限り)コーディングエージェントの核心が実際にはかなりシンプルだってことを指摘した最初の記事だったんだ(深い複雑さはLLMにある)。これを読んだときは目から鱗だったよ。ちなみに、ここにいる他のコメント者たちと同意見だけど、現代のエージェントをうまく機能させるにはかなりの追加の足場(TODOとかもっと色々)が必要だと思う。そしてクロードコード自体は設定やフック、プラグイン、UI機能がたくさんあるかなり複雑なソフトウェアだよ。ただ、ミニマルなコーディングエージェントループができれば、自分でコードをブートストラップしてそれらを追加できるようになるんだ!それは試してみると楽しいし、ちょっと変わったことだよ。(ところで、この記事の日付「2025年1月」は明らかに2026年のタイプミスだね。クロードコードは1年前には存在しなかったし、2025年5月のclaude-sonnet-4-20250514モデルを使ってるから。)編集: もしクロードコードが裏で何をしているのかもっと深く知りたいなら、「claude-trace」という良いツールがあるよ(https://github.com/badlogic/lemmy/tree/main/apps/claude-trac...)。これを使うと、ツール呼び出しとLLMの全体の流れを見られるよ。LLMへのすべての呼び出しとその応答、LLMのツール呼び出しの呼び出しと、ツールが実行されるときのエージェントからLLMへの応答などがね。クロードスキルが出たとき、これを使ってどうやって機能するのかの予想を確認したよ(ツールの説明に短いスキルの説明が詰め込まれたツール呼び出しなんだ)。基本プロンプトを読むのも面白いよ。(他にも、絵文字を使わないように明示的に指示してるんだけど、私が自分のエージェントを書いたときは確かに絵文字が多かったから、それが分かる。)
あなたの言葉によると、コーディングエージェントの>>コアは本当にシンプルだそうですが、見せてくれますか?それと、チェックできるURLも教えてもらえますか?
一番重要なのはコードの編集ですね。確実にそれを行うために、Claudeモデルは独自の文字列置換ツールのスキーマで訓練されていると思います。モデルは既存のコードを修正するのが難しいし、全ファイルを再記述するのもコストがかかるし、スケールしません。
Claude CodeとCodexの内部を探っていて、彼らがローカルで生成するトランスクリプトを見ているんだ(これが製品とのインタラクションの唯一の記録になる)。記事の立場を考えると、トランスクリプトのフォーマットだけでも、掘り下げると驚くほど複雑なシステムが見えてくるよ。Claude Codeの場合、基本的なユーザー/アシスタントのループを超えて、会話のチェーンのためのuuid/parentUuidのスレッド、ツール実行中に送信されたメッセージを処理するためのキュー操作記録、ファイルの変更ごとのファイル履歴スナップショット、タスクツールが並行作業者を生成する際のサブエージェントのサイドチェーン(agent-*.jsonlファイル)などがある。だから「200行」というのは概念を捉えているけど、実際の生産現実を反映しているわけではない。特にCodexはまだキューイングを出荷していないのが注目すべき点で、この製品は多くの注目を集めていて、まだ非常に能力が高い。私はContextify(https://contextify.sh)というmacOSアプリを作っていて、Claude CodeとCodexのCLIトランスクリプトをリアルタイムで監視し、両方のプロバイダーにわたる会話履歴をクエリするためのCLIとスキル「Total Recall」を提供している。今、Linux版をリリースするところで、フィードバックがあれば嬉しいな。[1] Claude Code Webを除いて、これはローカルとホストされた実行環境間で「セッション」や共有トランスクリプトを公開している。
ああ、同じ記事が再投稿されたのかと思ってた。
私たち(SWE-benchチーム)は、今や学術界や業界のラボでかなり人気のある100行のコードエージェントを持っています。こちらです: https://github.com/SWE-agent/mini-swe-agent これはエージェントの世界に飛び込むのに素晴らしい方法だと思います。
追加したいのは計画の重要性。これらのツールを効果的に使うための大きな「気づき」は、動的なTODOリストで動いていることを理解することだよ。例えば、プランモードはそのTODOリストがどうやって種をまくか、そしてTODOが達成されたときにどうやって自分を整えるかをブートストラップするようなもの。ユーザーのインタラクションがTODOリストを再調整する方法なんだ。TODOリストは微妙だけど、コーディングツールにおいて大きな変化だったし、これについて話すと多くの人が驚くみたい。ほとんどの人はプランモードを使うべきかどうかに焦点を当てるけど、TODOリストは常にアクティブなんだ。先月、Claude CodeがCTFをどれだけうまく解決するかの面白い実験をしたんだけど、TODOリストツールと計画を無効にすると1-2段階の向上があったよ: https://media.ccc.de/v/39c3-breaking-bots-cheating-at-blue-t... 。ちなみに、記事が「よりスマートなコンテキスト管理」を生産レベルにするための軽いTODOの箇条書きに詰め込んでいるのが面白かった。自分たちでやれると思っているNIH/DIYタイプの人たちが多いけど、結果や評価が生産で良くないと強いられると、そのステップで残りの年を無駄にすることになるんだよね。(さらに悪いのは、微調整を決めるとき。)
正確性や出所、古さについては自信ないけど、これがClaude Codeのために抽出されたシステムプロンプトらしいんだ。TODOの反復やその力について詳しく書いてあるよ: https://gist.github.com/wong2/e0f34aac66caf890a332f7b6f9e2ba... https://gist.github.com/wong2/e0f34aac66caf890a332f7b6f9e2ba... 理論的には、これを推論トークンとして文脈に追加して、注意アルゴリズムが最新のTODOリストを見つけてアクティブに処理することができるはずなんだけど、実際には、単一キーのストレージを行う明示的なツールを作る方がずっと効果的で予測可能だよね。言語を強調して構造化するためのツール作成には、他にもどれだけの簡単なアイデアがあるのか気になるな。
年末には「200百万行のコードでClaude Codeを書く方法」ってなるよね :)
TODOリストは、LLMが過去のステップや次のステップを意識できるように、文脈のHEADに頻繁に再挿入されるんだ。文脈圧縮が起きた場合、TODOはセッションのコンパクトな表現として機能する。
そうそう、僕はよく「このタスクにはすごく細かいTODOリストを使ってね」っていうのをプロンプトの最後に追加するんだ。たまに「最後のTODOとして、今やったことをもう一度見直して、リンターや他のツールを使って質が高いか確認してね」って言ったりもするよ。
CLIエージェント用に「作業メモ」ファイルを作ることで、めっちゃ成功してるんだ。今はCodexを試してるところで、まずは約10分かけて仕様を考えて、それを変更リストに分けるんだ。それからエージェントにその変更をファイルに保存させて、作業を進めるごとにそのファイルを更新させるのがポイント。重要なのは、毎回変更の後に計画を見直して、必要なら修正するように指示すること。これでLLMが得意なこと(限られたコンテキストでの短期目標)を続けられるし、常にプロンプトを出す必要もなくなる。要するに、同じような結果を得るためのサブエージェントの代わりみたいな感じだね。
2023年の「100行のコードでLangChainを再実装する」という投稿を思い出す: https://blog.scottlogic.com/2023/05/04/langchain-mini.html その時、私たちはまさにそれをやって、うまくいったし、その後多くのプロジェクトで使ったよ。
これ、3年前のことなんだよね ;_;
ちなみに、関連情報として「エージェントを書くべき理由¹」や「エージェントの作り方²」があるよ。¹ https://fly.io/blog/everyone-write-an-agent/ ² https://ampcode.com/how-to-build-an-agent
> これが重要な洞察なんだ:我々はLLMに「これがツールだよ、呼び出すフォーマットはこれだよ」って言ってるだけ。LLMはそれをいつ、どう使うかを考えるんだ。これ、2024年頃の古代の時代に本当に衝撃的だった。エージェントのアイデアがやっと私のところに届いて、いろんな「ここにエージェントを作ったよ」って記事を読み始めたんだけど、LLMがどうやってツールを呼び出すのか全然理解できなくて、本当にイライラした。プログラムなのに、LLMはただテキストを生成するだけじゃん!ツールについてLLMに教えてるのはわかるけど、その後はどうするの?そして、結局次はない、説明するだけでいいって理解したときは、ちょっと魔法みたいに感じたよ、正直。
これ、すごいけど、月に10億トークン以上を処理するエンタープライズグレードのエージェントループを社内で作った身としては、現実のエージェント利用ケースで考慮しなきゃいけない小さなことがたくさんあって、複雑さが大きく増すんだよね。forループは入り口としては簡単だけど、全体の中心にあるのは確かだよ。でも、複雑さをすぐに増す小さなことがたくさんあるんだ。ユーザーが最初のメッセージの後にメッセージを送ったとき、エージェントがすでにツールループを始めてたらどうなる?簡単そうに見えるよね?もしウェブフック(Slackボットからのような)で入力を受け取ってるなら、どうする?ロケットサイエンスではないけど、ちゃんとやるのは簡単じゃないよ。フック(ガードレール)や承認はどうする?ループの途中で実行を停止して待つべきか、それともClaude CodeやMCP仕様のように非同期タスク機能として実装するべきか?非同期にするなら、エージェントをどうやって再起動させる?元のツール呼び出しはどこに保存されてて、出力はどうやって保存して取り出す/挿入するの?これらの小さなことが積み重なって、互いに複雑さを増していくんだ。これについての経験をブログに書こうかな。
from ddgs import DDGS def web_search(query: str, max_results: int = 8) -> list[dict]: return DDGS().text(query, max_results=max_results)