メインコンテンツへスキップ
見出し画像

法令 Deep Research ツール Lawsy を OSS として公開しました

    「法令」×「デジタル」ハッカソンの成果物として開発した日本の法令に特化した Deep Research ツール Lawsy をデジタル公共財として OSS 公開しました。本記事では Lawsy の詳細と開発で得た Tips を公開します。

    Lawsy は下記の友人二人とチームで開発しました。

    本記事は、3人が力を合わせて作った Lawsy を自分の目線で振り返ってみた記事になります。

    (2025年3月16日更新)鈴木さんの記事も公開されました。本記事より背景について詳細に書かれており、俯瞰的でわかりやすいと思うのでぜひご覧ください。

    メンバーはみな本業があるためすきま時間を縫っての開発でしたが、久しぶりにスタートアップのド初期の全国制覇を目指す部活動のような開発ができて楽しかったです。お互いを信頼して Pull Request をどんどんマージしていったので、朝起きたら Lawsy に誰かが作ったすごい機能が入っていたりしてとても刺激的でした。

    Lawsy を使うと何ができるか

    Lawsy は、ユーザーのクエリーに対してWebの情報および法令情報をもとにDeep Research し、レポートを生成するシステムです。

    たとえば「ドラえもんに人権はある?」

    画像
    クエリー

    と聞くと1分程度で下記のようなレポートが返ってきます。

    画像
    生成されたレポート

    Lawsy は Perplexity や GenSpark、Felo の簡易版のような回答エンジンを日本の法令に最適化させてみようと作ってみたものですが、法令の条文をベクトル検索したり、検索対象のドメインを政府系ドメイン(go.jp)に絞り込んで Web 検索したりして、できるだけ事実に基づいた回答ができるように工夫しました。結果として、既存の汎用 Deep Research ツールと比べると日本の法令関連情報により密着した回答ができるようになったと考えています。

    開発期間が短く、チームにフロントエンドが得意なメンバーがいなかったのもあり、Lawsy のフロントエンドは全面的に Streamlit で作っていますが、Streamlit の使い方も改めて研究して結構工夫しました。裏側のロジックもちょっと手を加えれば法令以外の領域でも活用できるはずです。

    デジタル公共財とするべく、MIT ライセンスで Lawsy を公開することにしたので、自由に活用していただければ嬉しいです。自分もあちこちで使う予定です。そして、Lawsy をベースにもっとよいロジック・実装が現れ共有され、ハイスタンダードな RAG アプリケーションをもっと簡単に作れるようになっていくことを期待しています。

    Lawsyの生成ロジック

    実装にあたっては Stanford 大学から出ている STORM の実装を大いに参考にしました。

    STORM は、ユーザーが知りたいトピックにたいして Wikipedia 風の記事を生成する LLM を使ったツールで、OSS としてコードも公開されています。無料で試せる Webサービスも公開されているので興味のある方は試してみてください。STORM のコードは全面的に DSPy(ディーエスパイ)を使って書かれています。

    ※ DSPy に関してはこちらの記事がわかりやすいです。

    STORM ではおよそ以下の流れで記事生成が進みます。

    1. ユーザーのクエリーをクエリー展開(クエリーに関連する複数のリサーチトピックに分解)

    2. Wiki執筆者のペルソナを複数生成

    3. ペルソナ役とナレッジエキスパート役(検索も使える)に分かれて LLM が対話を通じて各トピックの情報を収集

    4. 収集された情報からアウトラインを生成

    5. 記事のドラフトを作成

    6. ドラフトにリード文をつけ推敲

    本家 STORM を試してもらうとわかりますが、記事が生成されるまでにそこそこ時間がかかります(数分くらい?)。Lawsy ではもっとライトな検索体験を目指したかったので、STORM における対話のステップ(3)の代わりに法令検索とドメインを絞った Web 検索で代用しました。

    以下が現状の Lawsy の処理フローです。

    1. ユーザーのクエリーを Web 検索用に変換

    2. ドメインを指定せず Web 検索

    3. 指定したドメイン(go.jp系)で Web 検索

    4. クエリー展開

    5. 各クエリーに対して法令ベクトル検索

    6. Text Embedding をもとに Web 検索結果と法令ベクトル検索結果をリランキング

    7. アウトライン(タイトル、セクション、サブセクション)をサブセクションごとに文献の引用番号を付与しながら生成

    8. サブセクションの文面生成

    9. 結論の生成

    10. リード文の生成

    最終的に、Agentic なフローを組んだりせず、ほぼ一直線な処理でそれなりの品質のレポートが生成できるようになりました。

    画像
    Lawsyの生成の流れ

    以下、上記のフローの技術的背景を説明します。

    なお、ハッカソンの本番用の LLM には OpenAI の GPT-4o を使いました。GPT-4o-mini でも割と安定的にレポート生成できることは確認しています。

    1. ユーザーのクエリーを Web 検索用に変換

    ユーザーのクエリーはときに非常に長くなることがあります。

    画像
    少し長めのクエリー(これはまだ短い)

    試作版でいろいろ遊んでいるときに、クエリーが長すぎて後段の Web 検索がクエリーの文字列長の上限エラーになることがありました。たとえば Web 検索に Tavily を使う場合だと、400 文字が上限になるようです。そのため、クエリーを Web 検索用に短く要約する機構を入れました。

    下記は Lawsy 内の DSPy での実装です。DSPy では宣言的に LLM での処理を記述することができます。クラスの docstring 部分が指示、InputField が入力、OutputField が出力となります。

    class RefineQuery(dspy.Signature):
        """
        あなたはWebの扱いに長けた優秀な法律家です。
        下記のユーザーのクエリーにたいして、法令観点からの回答を与えてくれそうな簡潔なクエリーを一つ作ってください。
        なお、作成にあたっては下記を守って下さい。
    
        - 法令観点からの回答を与えてくれそうなクエリーをつくること
        - あなたの作ったクエリーは可能な限りユーザーが作ったクエリーの意図と過不足なく一致させること
        - クエリーは日本語で作成すること
        - Web検索に最適化すること
        - 簡潔であること
        """
    
        query = dspy.InputField(desc="ユーザーのクエリー", format=str)
        refined_query = dspy.OutputField(desc="あなたの作成したクエリー", format=str)

    この RefineQuery を使うと、こんな感じでユーザーのクエリーが変換されます。

    日本には全省庁統一資格というものがあり、企業の資金力やに応じて法人の等級が決まり、東急に応じて入札できる国の案件の金額レンジが変わるということを知りました。

    しかし、これでは案件にマッチした尖った技術を持ったスタートアップであっても資金力等々が基準を満たさず、国の大規模案件へ主体的に参画することが難しくなる(実質不可能になる)のではと想像しています。この理解は正しいでしょうか?

    技術を持ち国家に貢献する気概のあるスタートアップや個人が国の大規模案件に主体的に関わる方法はなにかあるのでしょうか?

    入力クエリー

    ↓ ↓ ↓ ↓ ↓

    日本の国の入札制度におけるスタートアップの参入方法と資格基準の影響

    出力クエリー

    2. ドメインを指定せず Web 検索

    Lawsy を開発している最中に八潮市の道路陥没の事故が発生しました。Lawsy は八潮市の事故についてなにか回答を生成できるでしょうか?

    LLM の学習に使われたデータの鮮度にはカットオフがあるため、LLM 単体では最新の情報に回答することはできません。そこで、最初にドメイン指定なしの Web 検索を行い Web から鮮度の高い情報を得ることにしました。

    画像
    ニュースに対応するために Web 検索を最初にする

    検索結果を LLM で参照するには、検索でヒットしたページに関するテキスト情報が必要です。精確さを求めるなら、ヒットしたページを開いて解析するべきですが、ヒットしたページに負荷をかけすぎたくなかったのと実装の時間もあまりなかったので、今回は Web 検索エンジンが返す snippet をそのまま参照することにしました。

    ハッカソン本番用の Web 検索エンジンには Tavily を使いました。Google 検索も試したのですが、残念ながら snippet がスパース過ぎてそのままでは使いづらかったです。個人でサクッと試すときは rate limit はありますが API キーもいらないですし、DuckDuckGo などでも良いかもしれません。本当はBing 検索や Serp も試したかったのですがまだ試せていません。

    Web 検索エンジンを選ぶ際に snippet がどんな感じなのかをパッと参照できる記事などがすぐに見つからなかったので、参考までに Google 検索、Tavily、DuckDuckGo の snippet を載せておきます。

    クエリー: 「「法令」x「デジタル」ハッカソン」

    画像
    Google の検索結果
    画像
    Tavily の検索結果
    画像
    DuckDuckGo の検索結果

    上記の例でも発生していますが、Tavily だと PDF のタイトルが単に「PDF」になってしまったりすることもあり、その点はやや不満が残ります。ただ、試した中では割と長めの snippet を返してくれる印象で RAG 用途では使いやすそうでした。なお、Tavily は日本語で検索すると文字化けした snippet を返すことがあります。我々の対処方法は後述の Tips に記載したのでご興味ある方はご覧ください。

    Web 検索エンジンの選定と最適化にはまだまだ改善の余地があるのですが、ハッカソンなのでいったん Tavily をそのまま使うだけで済ませています。

    RAG に最適化された Web 検索エンジンなどがもっと充実すると良いなと思いました。

    3. ドメインを指定して Web 検索

    Lawsy ではなるべく事実情報に基づいたレポート生成をさせたかったので、チームメンバーの鈴木さん(現役官僚!)のドメインナレッジをもとに政府系のドメイン(go.jp など)に限定した Web 検索も行いました。ドメイン指定の有無でかなり検索結果が異なります。ドメインを絞る分、あまり関係のなさそうな結果も返ってくることがあるのですが、質の良い情報に基づいた回答を作成するためには目的に応じたドメインの指定は重要だと感じました。

    画像
    ドメイン指定なし・ありの検索結果

    4. クエリー展開

    ユーザーの入力したクエリーの意図を汲み取り、複数のリサーチすべきトピックへクエリー展開をします。DSPy での定義は下記のようになります。

    class GenerateDetailedTopics(dspy.Signature):
        """あなたは日本の法令に精通した専門家です。
        下記のクエリーに関して、事前にWeb検索をして簡単に下調べしてあります。
        Web検索結果もふまえ、クエリーに関する法令の解説・解釈文書を作成するために必要な法令を検索しようとしています。
        法令検索はセマンティックサーチできるのでキーワード・短文などで検索可能です。
    
        解説・解釈に必要な法令を適切にヒットさせきるための検索トピックを以下の形式でリストアップしてください。
    
        出力フォーマット:
        - xxx
        - yyy
        - ...
        - zzz
    
        検索トピックをリストアップするにあたり、以下の条件を遵守してください。
    
        - クエリーの意味を十分汲み取ること
        - クエリーに対する法令解説・解釈を行うのに必要な法令が揃うように検索トピックをリストアップしてください
        - 一般的な用語は法令での専門用語を優先してください。法令用語が含まれていない場合は、該当しそうな法令用語や関連用語を補完してください
        - 検索トピックは可能な限り互いに重複せず、個別に調査可能な形にしてください。 **self-contained** であるべきです
        - 検索トピックは法令コーパスに準拠した具体的なものにしてください(短文または具体的な法令用語)
        - 検索精度を高めるため、法令での使用が想定される具体的な専門用語や関連する条文番号を含めてください
        - 検索トピックをナンバリングする必要はありません。検索トピックの内容のみ記載してください
        - 検索トピックの個数は多くても10個までにしてください。多ければよいというわけではないです
        """
    
        query = dspy.InputField(desc="クエリー", format=str)
        web_search_results = dspy.InputField(desc="Web検索結果", format=str)
        topics = dspy.OutputField(desc="リストアップされた検索トピック", format=str)

    LLM を呼び出すだけで何も考えずにクエリー展開が実現できてしまうのは、いい時代になりましたね。

    5. 各クエリーに対して法令ベクトル検索

    法令に対してベクトル検索をします。が、法令の XML データは公開されているもののベクトル検索レディなものではないので、法令をパースした上で、以下のように親階層つきの 1 条 1 チャンク構成に変換しました。附則も含めてですが全体でおよそ 516,277 チャンクになりました。これが人手で書かれて厳密に運用・メンテされていると考えると、凄まじいですね。

    教科用図書検定規則
      本則
        第三章 検定済図書の訂正等
          第十五条の二 (参照するウェブサイトの内容の変更の手続)
          検定を経た図書について、当該図書中にウェブサイトのアドレス(二次元コードその他のこれに代わるものを含む。)が記載されている場合であって、当該ウェブサイトの内容を変更しようとするときは、発行者は、文部科学大臣が別に定める日までにあらかじめ文部科学大臣へ報告するものとする。
          前項の報告をしようとする者は、文部科学大臣が別に定める様式による変更報告書を文部科学大臣に提出するものとする。

    個人的にはこのパース部分が一番手間取りました。法令の条文で別法令を参照していたりして構造が再帰することもありますし、そもそも法令にもその XML 構造にも不慣れな自分にとっては、必要な情報を正確に抽出していくのがいちいち大変でした。出来上がってしまえば大した処理ではないのですが…

    最終的に数件の例外的なケースが発生しましたが、それらは今回無視しました。本番運用するならケアが必要だと思います。

    法令 XML のパースには、ja-law-parser を便利に使わせていただきました。大変助かりました。ありがとうございます。

    こうして出来上がった法令の条文チャンクを OpenAI の text-embedding-3-small を用いてベクトル化しました。非並列でやったのですが2日くらいかかりました。バッチ API は使ってないですが、コストはおそらく数百円程度だったと思います。安い!

    実は当初は mutilingual-e5-large-instruct でベクトル化していたのですが、それだと手元の GPU マシンで 1 時間ちょっとベクトル化処理が完了していました。text-embedding-3-small よりだいぶ高速です。しかし、Lawsy の開発中は Cloud Run にデプロイして誰でも試せるようにしており、なるべく重たいモデルは同梱したくなありませんでした。そのため、本番では OpenAI の API を使うことにしました。

    今回自作したベクトル検索のように、法令のセマンティック検索ができる API がだれでも自由に使えるようになると、いろんなアイデアを気軽に試せるようになります。新しい法令の公開・施行に合わせてベクトルデータの差分更新をし続ける必要はありますが、誰もが使える信頼性の高いデファクトスタンダードなベクトル検索 / API があるともっと法令データを活用しやすくなるはずです。

    法令の自由な AI 活用のためには法令を気軽にセマンティック検索できる機能が公開されることが重要に感じます。それが公開されればアイデアを圧倒的に簡単に試せるようになるはずです。

    6. Text Embedding をもとに Web 検索結果と法令ベクトル検索結果をリランキング

    (ドメイン指定有り・無しの)Web 検索結果と法令の条文のベクトル検索結果をいい感じに混ぜて関連度順に並べ替えます。複数の検索結果をどう fusion させようかと悩みましたが、法令の条文は Text Embedding してベクトル化してあるのもあり、Web 検索結果の snippet を on-the-fly で Text Embedding し、クエリーに対する近さでベクトル検索することにしました。

    クエリー側は

    以下の内容に関する法令解説文書を作るにあたって参考になるWebページや法令がほしい
    
    主題となるクエリー:  {{入力クエリー}}
    関連するトピック: {{展開されたトピック}}

    のような頭書きをつけてベクトル化し、ベクトル類似度順に並び替えています。

    リランキングもちゃんとやろうとするといろいろ工夫の余地はあるのでしょうが、実は今の設定では後段でほぼ全部の検索結果を参照させて RAG をできてしまっているので、現状のリランキングは上の方に重要そうな文献が来るように見た目を整えるくらいの効果しかないです。何も入れないのは気持ちが悪いし、ワーキングベースラインとしては悪くないかなと思っています。

    7. アウトライン(タイトル、セクション、サブセクション)をサブセクションごとに文献の引用番号を付与しながら生成

    リランキングされた検索結果から LLM の入力トークン長が上限を超えないように適当にランキング上位の文献を取ってきて RAG によりアウトラインを生成させます。

    先述の通り、現状の実装では 100 件以上の検索結果を参照させられているので、実質的に検索で得たほぼ全てのナレッジが参照されています。ここはコスト的にはあまり美味しくないのですが目をつぶりました。例えば GPT-4o だと 1 回の実行に数十円かかる場合もあります。ライトに試したい場合は GPT-4o-mini などが良いかもしれません(それほど精度劣化は感じませんでした)。

    DSPy だとこんな感じです。

    class CreateOutline(dspy.Signature):
        """あなたは、ニュースを常にフォローしつつ、日本の法令に精通している専門家です。収集された情報源をもとに、下記のクエリーに対するレポートとして適切なアウトラインと簡潔なタイトルを作成してください。また、最後にレポートのタイトルを作成してください。結論パートは絶対に作成しないでください。
    
        アウトラインは以下のMarkdownフォーマットに従って作成し、次のルールを厳守すること。
        1. クエリーに対して、法令観点での解説・解釈を含む回答を目的とした構成にする。
        2. "# Title" をレポートのタイトルに用いる。
        3. "## Title" をセクションのタイトルとして用いる。
           - 各セクション ("## Title") に対して、必ず2~3個以上のサブセクション ("### Title") を生成すること
           - セクション数はクエリーに応じて5~20個の間で作成すること。網羅的な回答が必要な場合、セクション数は多い方が好ましい
        4. "### Title" をサブセクションのタイトルとして用いる。
           - 各サブセクションには、収集された情報源をできるだけ多く含めるようにし、必ず最低3~5個以上の異なる引用番号を使用すること
           - サブセクション同士で内容が類似しそうな場合(情報源の法令・条文が同じ、タイトルが酷似)、サブセクションは統合し、他の論点のサブセクションを追加すること
           - サブセクションの引用番号が他と重複する場合、より多様な情報源を取り入れるために、アウトライン全体を統合・再編成すること
           - 必要に応じて、さらに下位の階層も同様に2つ以上生成すること。
        5. 引用情報の記載方法
           - 今後必要となるので、"### Title"のサブセクションタイトル作成の際には参照した条文の引用番号を記載
           - 引用番号はサブセクションの次の行に記載すること。セクション、サブセクションの行には引用番号はつけない。
           - "### Title [3][4]" のように同じ行に引用番号を記載してはならない
        6. Markdownフォーマットに関するルール
           - "## 結論"という結論パートは絶対に作成してはいけない
           - 出力には "# Title", "## Title"、"### Title"、"#### Title" などのMarkdown形式のタイトル以外のテキストを一切含めないこと
           - "#### Title"というサブサブセクションは作成しないこと
           - ナンバリングは不要
           - "#", "##", "###" の階層のみ作成すること
        7. 各行はタイトル、セクション、サブセクション、引用番号のいずれかでそれ以外の情報を記載してはならない
    
        【出力例】
        # レポートタイトル
        ## セクション1
        ### サブセクション1
        [4][67]
        ### サブセクション2
        [30][28][1][27][102]
        ## セクション2
        ### サブセクション1
        [14][25][9][96]
        ### サブセクション2
        [2][24][51]
        ### サブセクション3
        [29][11][4][56]
        ...
    
        【不適切な出力例】
        # レポートタイトル: xxx    // 「レポートタイトル: 」という修飾はNG
        ## セクション1
        ### サブセクション1 [4][67]    // 引用はサブセクションの行にはつけない
        [4][67]
        このサブセクションでは...    // 説明文など要求していない不要な行は削除
        ### サブセクション2 [30][28][1][27][102]    // 同上
        [30][28][1][27][102]
        ## セクション2
        [2][24][29][11][4]    // セクション自体に引用がついている
        ### サブセクション1
        ### サブセクション: yyy    // 「サブセクション: 」という修飾はNG
        [2][24][51]
        ### サブセクション3
        [29][11][4][156]     // (引用が例えば110までしかない場合)156は存在しない引用
        """

    アウトライン生成の段階で各サブセクションで引用すべきナレッジの引用番号を付与させておき、後段のサブセクションの文面生成時には指定した引用だけを読み込むようにし、無駄なナレッジを参照しないで済むようにしています。

    GPT-4o(-mini) だと割と安定して生成できているのですが、ハッカソン中に登場して実用性が非常に高いと評判だった gemini-2.0-flash-001 だとアウトライン生成で意図せぬ生成をしがちでした。gemini-2.0-flash-001 でも使えるようにルールを色々追加したのですが、結局、構造化出力させない gemini-2.0-flash-001 は制御しきれなかったので、今回は gemini-2.0-flash-001 の使用は諦めました。アウトライン生成の高速化・安定化は今後の課題です。

    なお、稀にですが(gemini-2.0-flash-001 だとしばしば)不適切な形式でアウトラインが生成されることがあったので、アウトラインを修正する下記のようなルーチンも実行しています。

    class FixOutline(dspy.Signature):
        """あなたは校正や編集業務に定評のある腕利きのアシスタントです。
        収集された情報源をもとに、法令解説・解釈レポートのアウトラインを作ったのですが、一部、下記のアウトライン作成ルールを逸脱している可能性があります。
        下記のルールを守るように校正・編集してください。
    
        1. クエリーに対して、法令観点での解説・解釈を含む回答を目的とした構成にする。
        2. "# Title" をレポートのタイトルに用いる。
        3. "## Title" をセクションのタイトルとして用いる。
           - 各セクション ("## Title") に対して、必ず2~3個以上のサブセクション ("### Title") を生成すること
           - セクション数はクエリーに応じて5~20個の間で作成すること。網羅的な回答が必要な場合、セクション数は多い方が好ましい
        4. "### Title" をサブセクションのタイトルとして用いる。
           - 各サブセクションには、収集された情報源をできるだけ多く含めるようにし、必ず最低3~5個以上の異なる引用番号を使用すること
           - サブセクション同士で内容が類似しそうな場合(情報源の法令・条文が同じ、タイトルが酷似)、サブセクションは統合し、他の論点のサブセクションを追加すること
           - サブセクションの引用番号が他と重複する場合、より多様な情報源を取り入れるために、アウトライン全体を統合・再編成すること
           - 必要に応じて、さらに下位の階層も同様に2つ以上生成すること。
        5. 引用情報の記載方法
           - 今後必要となるので、"### Title"のサブセクションタイトル作成の際には参照した条文の引用番号を記載
           - 引用番号はサブセクションの次の行に記載すること。
           - "### Title [3][4]" のように同じ行に引用番号を記載してはならない
           - 存在しない引用番号を引用してはならない
        6. Markdownフォーマットに関するルール
           - "## 結論"という結論パートは絶対に作成してはいけない
           - 出力には "# Title", "## Title"、"### Title"、"#### Title" などのMarkdown形式のタイトル以外のテキストを一切含めないこと
           - "#### Title"というサブサブセクションは作成しないこと
           - ナンバリングは不要
           - "#", "##", "###" の階層のみ作成すること
        7. 各行はタイトル、セクション、サブセクション、引用番号のいずれかでそれ以外情報を決して記載しない
    
    
        【修正が必要な入力アウトライン例】
    
        ```
        # レポートタイトル: xxx    // 「レポートタイトル: 」という修飾はNG
        ## セクション1
        ### サブセクション1 [4][67]    // 引用はサブセクションの行にはつけない
        [4][67]
        このサブセクションでは...    // 説明文など要求していない不要な行は削除
        ### サブセクション2 [30][28][1][27][102]    // 同上
        [30][28][1][27][102]
        ## セクション2
        [2][24][29][11][4]    // セクション自体に引用がついている
        ### サブセクション1
        ### サブセクション: yyy    // 「サブセクション: 」という修飾はNG
        [2][24][51]
        ### サブセクション3
        [29][11][4][56]
        ```
    
        【修正済みアウトライン例】
    
        ```
        # xxx
        ## セクション1
        ### サブセクション1
        [4][67]
        ### サブセクション2
        [30][28][1][27][102]
        ## セクション2
        ### サブセクション1
        [14][25][9]
        ### yyy
        [2][24][51]
        ### サブセクション3
        [29][11][4][56]
        ```
        """  # noqa: E501
    
        outline = dspy.InputField(desc="入力アウトライン", foramt=str)
        fixed_outline = dspy.OutputField(desc="修正済みアウトライン", format=str)

    8. サブセクションの文面生成

    サブセクションごとに独立に文面生成をします。Lawsy では並行処理でストリーム生成させています。アウトライン時点で参照すべきナレッジを指定しているので、ここではそこで指定したナレッジのみを参照させることで軽量化を図っています。

    DSPy の実装は下記です。

    class WriteSection(dspy.Signature):
        """あなたは日本の法令に精通し、適切な法令解釈を行い、分かりやすい解説を書くことに定評のある信頼できる粘り強いライターです。
        下記のクエリーに関する調査をしており、クエリーをもとにレポートのアウトラインを作成しました。
        アウトラインの中にある引用番号は漏れることなく必ず参照し、収集された情報源の内容を適切に解釈しながら各セクションの内容を記載してください。必ず各サブセクションごとに400字以上記載してください。
        解説は緻密かつ包括的で情報量が多く、情報源に基づいたものであることが望ましいです。法令に詳しくない人向けにわかりやすくかみ砕いて説明することも重要です。必要に応じて、用いている法令の概要、関連法規、適切な事例、歴史的背景、最新の判例などを盛り込んでください。
        なお、内容の信頼性が重要なので、必ず情報源にあたり、下記指示にあるように引用をするのを忘れないで下さい。
        1. アウトラインの"# Title"、"## Title"、"### Title"のタイトルは変更しないでください。
        2. 必ず情報源の情報に基づき記載し、ハルシネーションに気をつけること。
           記載の根拠となる参照すべき情報源は "...です[4][1][27]。" "...ます[21][9]。" のように明示。
           その記述に対しての関連性が高そうな順に付与してください。
        3. 正しく引用を明示されているほどあなたの解説は高く評価されます。
           引用なしの創作は論拠が明確でない限り全く評価されません。
        4. 情報源を解説の末尾に含める必要はありません。
        5. 日本語のですます調で解説を書いてください。
        6. 引用は文面を引用する論理的な必要性がない限り、引用番号の引用のみにしてください。
        7. 「収集された情報源と引用番号」にない番号を引用しないでください。それは創作になってしまい価値を既存します。
        """
    
        query = dspy.InputField(desc="クエリー", format=str)
        references = dspy.InputField(desc="収集された情報源と引用番号", format=str)
        section_outline = dspy.InputField(desc="セクションのアウトライン", format=str)
        section = dspy.OutputField(desc="生成されたセクション", format=str)

    9. 結論の生成

    サブセクションの文面をもとに結論の生成をします。特別な工夫はしていません。

    class WriteConclusion(dspy.Signature):
        """あなたは日本の法令に精通し、分かりやすい解説を書くことに定評のある信頼できるライターです。
        レポートのドラフトを踏まえて、レポート全体の要約を本文とはできるだけ異なる表現で記載しつつ、今後の方向性や対応策を含んだ結論部の中身を生成します。
        最低でも400字以上、可能なら600字以上記載してください。
        結論の文章部分のみ生成し、"## 結論" のようなヘッダは入れないでください。
        """
    
        query = dspy.InputField(desc="クエリー", format=str)
        report_draft = dspy.InputField(desc="レポートのドラフト", format=str)
        conclusion = dspy.OutputField(desc="生成された結論", format=str)

    10. リード文の生成

    同じく特にこれといった工夫はしていません。

    class WriteLead(dspy.Signature):
        """あなたは日本の法令に精通し、分かりやすい解説を書くことに定評のある信頼できるライターです。
        下記のクエリーに関する調査をしており、レポートを作成しました。レポートタイトルの直後に表示する簡潔なリード文を生成してください。
        リード文とは、レポート全体のabstractとなる、レポート全体の論旨の展開や主要トピックを含めた簡潔な文章です。
    
        リード文の生成にあたって次のルールを厳守すること
        - リード文では、レポート全体の文脈やレポートで扱われている主要なトピックに対する簡潔な概要を提示し、それ単独でも読める内容にすること
        - リード文の文字数は140〜280文字程度とすること
        - リード文の文面のみ生成すること。
        """
    
        query = dspy.InputField(desc="クエリー", format=str)
        title = dspy.InputField(desc="レポートのタイトル", format=str)
        draft = dspy.InputField(desc="レポート内容", format=str)
        lead = dspy.OutputField(desc="リード文の文面", format=str)

    Tips

    細かいところでは色々工夫をしました。再利用できそうなものをいくつかご紹介します。

    Tavily の文字化け修正

    Tavily はどうも文字コードの扱いが苦手らしく、snippet が文字化けすることがあります(誤って latin-1 判定されているようです)。snippet によって文字化けしたりしなかったりするので、文字化けしたことをどう検知しようかと思案したのですが、言語判定を行い、判定結果が日本語でなかったら latin-1 として処理されているとみなすことにしました。リンク先を開いて html 解析をすればもっと確実に処理できるのでしょうが、先述の通り、ヒットしたページに負荷をかけたくなかったのでそれはやっていません。手元で確認している限りはこの方法でうまくいっているようです。

    コード的には下記のような感じです。

    async def fix_response(response: dict) -> dict:
        """Tavily seems not to handle charset properly, so fix it"""
        async def fix_result(result: dict) -> dict:
            result = result.copy()
            # langdetect cannot handle multiple lines
            try:
                det = langdetect.detect(result["content"].replace("\n", " "))
                if det["lang"] != "ja":
                    r = httpx.head(result["url"], timeout=1.0)
                    if r.encoding:
                        for key in ("content", "raw_content"):
                            if key in result and isinstance(result[key], str):
                                result[key] = result[key].encode("latin-1").decode(r.encoding)
            except Exception:
                ...
            return result

    Marp での Mindmap 表示

    文字文字しかったためアウトラインをそのまま Marp でマインドマップとして可視化しています(streamlit-markmap を使いました)。

    画像
    アウトラインをMarpでマインドマップに変換

    固定表示するために以下のディレクティブを入れています。

    ---
    markmap:
      pan: false
      zoom: false
    ---

    Streamlit 画面の狙った位置にあとから文章埋め込みをする

    下記のように、Streamlit で生成済みのテキストの途中に文章を挟み込みたくなることがあります。Lawsy ではアウトラインを生成・表示したのちに、サブセクションの文面を生成・差し込みをするので、これを実現する必要があります。

    画像
    事前に表示してあるアウトラインへの本文挟み込み

    Streamlit では、あとから挟み込みたい部分に empty オブジェクトを作っておくことで実現できます。

    たとえば、以下のような感じです。

    st.write("あああ")
    placeholder = st.empty()
    st.write("ううう")
    time.sleep(5)
    placeholder.write("いいい")

    上記の場合、最初に「あああ」「ううう」が描画された後、5秒たったあとに「あああ」と「ううう」の間に「いいい」が描画されます。

    Lawsy ではこの st.empty() で placeholder を作るテクニックをいたるところで使いました。

    Streamlit での並行ストリーム生成

    Lawsy ではアウトラインを生成後のサブセクションの生成は並行処理でストリーム生成しています。これも st.empty() で実現できます。具体的にはwrite_stream() でのストリーム描画を st.empty() で配置した placeholder に対して並行で行います。並行処理部分は asyncio をつかった普通の処理なのでここでは割愛するとして(必要であればソースコードをご覧ください)、概念的には以下のような感じになります。

    # placeholderを置いてから
    st.write("### subsection1")
    placeholder1 = st.empty()
    st.write("### subsection2")
    placeholder2 = st.empty()
    
    # write_streamで生成(実際は並行処理)
    placeholder1.write_stream(stream1)
    placeholder2.write_stream(stream2)

    ところが DSPy を使う場合、ストリーム生成はちょっと厄介です(うまい方法があれば教えて下さい!)。

    DSPy は内部的に独自書式で prompt を組み立て、生成結果を内部的にパースして出力を得る仕組みになっており、たとえば

    class QA(dspy.Signature):
        """Answer the query considering the relevant topics."""
        query = dspy.InputField(desc="input query", format=str, prefix="prefix1")
        topics = dspy.InputField(desc="relevant topics", format=str, prefix="prefix2")
        answer = dspy.OutputField(desc="your answer in markdown format", format=str, prefix="prefix3")

    は内部的には

    --------------------
    以下、system メッセージ
    --------------------
    Your input fields are:
    1. `query` (str): input query
    2. `topics` (str): relevant topics
    
    Your output fields are:
    1. `answer` (str): your answer in markdown format
    
    All interactions will be structured in the following way, with the appropriate values filled in.
    
    [[ ## query ## ]]
    {query}
    
    [[ ## topics ## ]]
    {topics}
    
    [[ ## answer ## ]]
    {answer}
    
    [[ ## completed ## ]]
    
    In adhering to this structure, your objective is: 
            Answer the query considering the relevant topics.
    
    --------------------
    以下、user メッセージ
    --------------------
    [[ ## query ## ]]
    宇宙にあるすべての物質の質量は?
    
    [[ ## topics ## ]]
    - 質量の物理学的な定義
    - 宇宙の起源
    - 宇宙にある全物質の質量の推定方法
    
    Respond with the corresponding output fields, starting with the field `[[ ## answer ## ]]`, and then ending with the marker for `[[ ## completed ## ]]`.

    のような内容の入力として処理されます。「以下、〇〇メッセージ」の部分は説明のためにつけています。

    そのため、DSPy を使って stream 生成させると、この

    [[ ## xxx ## ]] 

    形式のマーク部分もストリーム出力されてとても格好が悪いです(ストリーム処理でなければ DSPy が内部的に出力をパースしてくれるので問題ないのですが…)。そのため、Lawsy ではストリーム出力部分は(DSPy 内部で使われている)LiteLLM を生で呼び出してストリーム出力させるコードを自作して使うことにしました。

    なお、現状の LiteLLM をつかって async で stream 出力させると、LiteLLM の内部でつかわれている httpx のスレッドプールのハンドリングの不具合でエラーになってしまいます。以下に従って、litellm.acompletion() に  extra_headers={"Connection": "close"} を与えるとエラーを回避できます。

    よかった開発スタイル

    気心の通じた仲間と作ったのもありますが、Lawsy の開発は割とスムーズに行きました。個人的に良かったなと思う点をあげてみます。

    • メンバーにドメインエキスパートがいた。現役官僚の鈴木さんがチームメンバーだったのは大きかったです。ドメインナレッジに裏打ちされたコメントやアイデアは大いに役立ちました。また、他のメンバーからも自身の業務で遭遇した法令に関係する話題も出たりして、経験値(おじさん力)が活かされた感じがします

    • 参考にできる先行事例(STORM、Perplexity、GenStorm、Felo など)がありハイコンテキストな議論ができた。Lawsy の開発は 0→1 ではありませんでした。先行事例があったため、「〇〇にある✕✕みたいな機能を作りたい」というようなコミュニケーションがとれて大変スムーズでした。参考にすべきサービスの研究も結構行いました。そういうのに興味を持てるメンバーが集まっていたのも良かったです。自分は「あのサービスの〇〇すごいよね、どうやって作っているんだろう」という議論が純粋に楽しかったです。

    • 早めにベースライン実装を作れた。早い時期に Lawsy のベースライン実装を共有できたことで、プロダクトをベースにした建設的な議論や改善を行うことができました。既存のツールだとうまくできないがLawsy だとできそうなことも早い段階で見えてきました。

    • CI でのエラーチェックにたいする対応は徹底しつつ積極的にマージするようにした。全員本業持ちで開発や夜間か休日などのすきま時間でした。PR レビュー待ちで開発が滞るのがもったいなかったので、最終版に残りそうな機能はセルフジャッジでどんどんマージしていく方針にしました。ハッカソンであれば、開発のモメンタムを作る上でもこの方針で良かったと思いました。

    • ゆるく役割分担をきめて進めた。〇〇方面は✕✕さん担当というふうにゆるい役割分担を決めて進めたので、だれがどこを改善しているかがわかりやすくなりました。無論、領域侵犯もたくさんありましたが、作ろうしているものがある程度明確だったので、合理的なコミュニケーションをとりながら進めることができました。

    最後に

    自分はこれまで、どちらかというと法令とか契約書とかを敬遠して生きてきたのですが、この「法令」×「デジタル」ハッカソンに参加したことをきっかけに、生まれて初めて法律の教科書や政治家の方の著書を読んだりし、独特の面白さ・難しさを少しは知ることができました。このような学び・出会いの機会をご用意くださったハッカソンの企画・運営の方々に感謝しています。

    また、ハッカソンに誘ってくれたチームメンバーにもとても感謝しています。いい歳してハッカソンに出ていいのかなと最初はちょっと迷いましたが、気の知れた仲間との部活のようなプロダクト開発は純粋にとても楽しかったです。またなにか一緒に作りたいです。

    Lawsy は短い期間で開発したものなので色々と至らない部分があるのですが、もうちょっと時間があればああしたかったなと思えるのはいいプロダクト選定ができたからではないかなと思っています。Lawsy がデジタル公共財として、未来の「法令」×「デジタル」のベースライン実装として活用されると嬉しいです。法令の活用が少しでも Easy になりますように。

     
     
    合同会社nouu代表。機械学習、レコメンド、情報抽出、自然言語処理、画像処理、数理最適化、データ分析、データ基盤…などのテクノロジーの周辺を幅広く徘徊しています。

    あなたへのおすすめ