プログラミング
ドメイン駆動エージェント
Domain-Driven Agents (coldtake.dev)
要約
大規模言語モデル(LLM)をソフトウェアエンジニアリングに導入する際、特にレガシーコードベースでは、モデルの性能が低下する問題に直面します。これは、コードベースの複雑さや、ドメイン知識の欠如、共有言語の不在に起因します。この記事では、この課題に対処するため、ドメイン駆動設計(DDD)の概念をエージェントシステムに適用し、開発者が戦略的な意思決定に集中し、戦術的な実装をAIエージェントに委任するアプローチを提案しています。
全文翻訳
過去数年間、私はコーディング、あるいはより一般的にはソフトウェアエンジニアリングにおいて、大規模言語モデル(LLM)をヘビーに利用してきました。それによって得られる生産性の向上を何度も目の当たりにし、ますます多くのプロジェクトでLLMを使用するようになりました。新規プロジェクトや小規模なプロジェクトではうまく機能します。しかし、日々の業務では、重い依存関係ツリー、強い結合、そしてあらゆる未解決の問題を抱えた技術的負債のバックログを持つレガシーコードベースにエージェントを導入する必要があります。そこで、LLMが提供できる作業の質が急激に低下することにすぐに気づきます。その失敗には特定の形状があります。新規リポジトリで「求人オファーのステータス」フィールドを要求すると、それが得られます。しかし、4年間出荷されているシステムでそれを要求すると、モデルは、コードベース自体がどれが本物かを決定できなかったために、既に3回存在する概念の4番目のスペルを発明します。それは、呼び出しが問題なかった場所でアダプターを作成したり、アダプターが本来の目的であった場所で直接呼び出したりします。それらのそれぞれは、システムがどこにも答えていないシステムに関する質問です。モデルは推測し、しばしば間違った推測をします。したがって、ブラウンフィールドプロジェクトは深遠であり、技術的負債はその最初の層にすぎません。その下には第二の層があります:混乱、意味の欠如、そしてそれを解決するための共有言語の欠如。それがモデルが陥る層です。アップグレードが必要なのはモデルではありません。コードは準備ができておらず、その準備は私たちが構築できるものです。段階的に。少しずつ。私がどのように行うかをお見せしましょう。以前よりも簡単です。
ソフトウェアエンジニアリングの始まりには、唯一無二の存在がありました:技術的負債です。それは、私たち開発者が達成しようとしていることの自然な結果です。私たちは、将来のビジネス上の意思決定が現在のコードビューをシフトさせる準備ができていません。私たちは、ある程度のトレードオフを払いながら、迅速に提供する必要があります。その結果、コードの臭いはますます大きくなります。通常の回答は、クリーンアップにエンジニアリング予算の一部を費やすことです:技術的負債の解決のためにテクノロジー予算の10〜20%を割り当てることです。理論上は…次の四半期に…予算の5分の1は、何を変更すべきかを決定し、それをタイプアウトするためのコストであり、その2つの半分は決して同じ価格ではありませんでした。決定は以前と同じくらい高価なままでした。タイプアウトは崩壊しました。LLMは、もはや2020年とは似ても似つかないコストで、クリーンアップの機械的な半分(抽出されたモジュール、2つのパッケージにまたがるリファクタリング、より多くのテストカバレッジ)を実行します。技術的負債の支払いは依然として時間を要します。それは大幅に少ない時間を要し、私に残されたのは決定の部分です。
私は作業を2つに分割します。そして、ジョン・オースターハウトの「ソフトウェアデザインの哲学」の言葉を借りますが、正直に言って、それらを曲げていることを認めます。彼はコーディング中に持つことができる2つの態度として、戦術的と戦略的を使用します:戦術的プログラミングは「今すぐ動作させる」ことであり、戦略的プログラミングは設計に投資していくことです。私は、上記の経済性がその線に沿ってカットされるため、著者の分割に同じペアを使用します。戦略的な作業は決定することです:システムを読み、何を変更する必要があり、なぜ変更する必要があるのか、そしてその変更が実際に機能に役立つのかを理解することです。戦術的な作業は、その決定をファイルに反映させることです。前者はシステムを頭の中に入れる必要がある部分です。後者は安くなった部分です。
最初のパスでは私は完全に携わり、2番目のパスでは実装者というよりレビュー担当者です。最初のパスでは、コードベースをより一般的な方法で分析し、実装する必要がある変更とその変更が提供したい機能との整合性を評価します。これらのアプローチの効果は、私が各リポジトリに作成するGitHubイシューです。イシューはその後、スキルとサブエージェントに基づいて私のAIシステムによって処理されます。スキルとは、書き込まれた手順です:モデルがタスクに一致したときにロードする指示のマークダウンファイルです。そのため、「イシューを処理する」または「コンテキストマップを再生成する」は、その朝私がそれをフレーズ化した方法ではなく、毎回同じ方法で実行されます。サブエージェントとは、独自の新鮮なコンテキストと独自の狭いジョブ(実装、セキュリティレビュー、仕様に対するレビュー)を持つ個別のモデルセッションであり、トランスクリプト全体を私のものにダンプするのではなく、結果を報告します。それらが実装されると、PRはすぐに利用可能になります。私はレビューセッションを通過し、変更を受け入れるか、いくつかの改善を求めます。私はそれを段階的に行うことができ、テストカバレッジと誰が壊れるかを気にします:変更が着陸する前に、システムの他のどの部分が私が触っているものを消費しているのか、そしてその変更がそれらが生き残れるものなのかを知る必要があります。今、ソフトウェアエンジニアとして、私は調整し、計画し、改善のための道を作成します。しかし、その時点では、自分で実装する必要はありません。時間は節約されました。
それは戦略的な半分を残します、そしてそれはそれが書かれている言語と同じくらい価値があります。ここでDDDが登場します。DDDは常に私が1年後も変更できるソフトウェアの選択肢の1つでした。エリック・エバンスによって提示されたアプローチは、ビジネスと技術的な側面との間のコミュニケーションギャップを縮小する方法を提供してくれました。ユビキタス言語とバウンデッドコンテキストに基づいたドメイン駆動設計は、ビジネスが必要とするものを直接技術的な部分に翻訳します。両側は同じ言語で話します。ループにエージェントがいると、そのリンクはさらに重要になります:それは私たちがモデルに私たちのニーズを伝え、そして私たちがその推論を読み戻す方法です。だからこそ、私はそれに非常に重きを置いているのです。
私が所有するすべてのリポジトリには、ルートに.workflow.jsonがあります。それは私自身のマニフェストであり、リポジトリが私のツールにそれが何であるかを伝える場所です:どの言語が含まれているか、エージェントが最初に読み取るべきディレクトリ、作業が出荷される前にどのチェックをパスする必要があるか。その中の1つのブロックはドメインに関するもので、そのブロックを宣言することがリポジトリが必要とする唯一の登録です。同期から外れる第二のレジストリはありません。ブロックはプロジェクト、そのバウンデッドコンテキスト、各コンテキストの用語集がどこにあるか、そのサブドメインタイプ、そして隣接するコンテキストへのすべてのエッジの名前を付けます。例は私のプロジェクト、job-offer-boxから来ています。これは2つのリポジトリとして構築された求人応募トラッカーです。1つは私がhyperionプロジェクトの下で維持しているRustバックエンド、もう1つはWebフロントエンドです。ここにフロントエンドのマニフェストがあります。単一のエッジにトリミングされています:
{ "domain": { "project": "job-offer-box", "contexts": [ { "name": "job-box-web", "docs": "CONTEXT.md", "subdomain": "supporting", "edges": [ { "to": "hyperion/job-offer-backend", "direction": "outbound", "pattern": "unclassified", "owner": "supplier", "shape": "codegen from the backend's document (scripts/generate-api.ts:12) ... conformist on write (src/lib/api/jobs.ts:37), ACL on read (src/lib/api/adapters/offer.ts:50)", "note": "conformist on write and an anticorruption layer on read; two patterns hold at once, so neither name alone is true" } ] } ] } }
順に読んでください。toはアドレスです:反対側のどのコンテキストか。directionは誰が誰を呼び出しているかを示します:Webリポジトリはバックエンドを呼び出すのでoutbound(バックエンド自身のマニフェストは同じエッジをinboundと宣言しています)。ownerは、両側が最終的に意見の相違があった場合に、どちらのモデルが勝つかを示します:バックエンドのモデル、つまりsupplierです。patternは関係そのものであり、閉じた語彙から選択されます。ここではunclassifiedです。なぜなら、Webリポジトリは同時に2つの異なることを行っているからです。書き込み時にはバックエンドの形状をそのまま受け入れ、読み取り時には独自の形状に変換します。noteはそのことを明確に説明しています:単一のラベルは一方のケースについては正しく、もう一方のケースについては間違っているでしょう。マニフェストの隣には、コンテキストごとにCONTEXT.mdがあり、すべての用語の正確な意味と意図的に拒否された同義語を含む生きた用語集です。コンテキストごとに2つのファイルがあり、どちらもコードを所有するリポジトリによって所有されています。それらの上にあるものは何も作成されていません:コンテキストマップ(ポートフォリオ内のすべてのコンテキストとそれらの間のすべてのエッジを示す1つのドキュメント)は派生されたものです。ジェネレータ、つまりディスク上のすべてのリポジトリをウォークするスクリプトは、domainブロックをユニオンし、それを単一のCONTEXT-MAP.mdとして出力します。マップは使い捨て可能で再生成可能です。
joに戻って