🍣

「一次情報+証拠」をブラウザ操作とLLMでリサーチ自動化した際の実装メモ

に公開

この記事は、ナウキャスト Advent Calendar 2025 の6日目の記事です。

初めまして!株式会社ナウキャストでインターンをさせてもらってます、大学生のロゲメガです。

普段は大学生活の他に、42Tokyoというところでプログラミングのお勉強をしたり、趣味で株をやってたりします。
ご縁があって、2024年の夏の短期インターンをさせてもらったことがきっかけで、2025年の春頃から長期インターンに入らせてもらっています。

この記事では、長期インターンの中で夏頃に取り組んだことについて、簡単にまとめてみたものです。

はじめに

米国株の企業情報を「一次情報にあたり、証拠付きで提出する」業務をLLMで代替できるか検証しました。銘柄コードを入力として、創業年や設立州(登記日)などを公式ソースから集め、PDF/HTML証拠とURLリストを添えてJSONにまとめることをゴールにしています。ここでは、実際に組んだフローと、うまくいった工夫・つまずきポイントを共有します。

挿絵は、Nano Banana Proを使ってポン出ししてみたものです。

今回の問題設定は米国株の企業情報の収集タスクですが、何かの情報をLLMに収集してほしいし、参照情報は一次情報で証拠付きが求められる、という場面は他にもあると思います。そうした場面での参考になれば幸いです。

取り組みの前提とスコープ

  • 入力: 米国株のティッカーコード(例: AMZN)
  • 一次情報の優先順位: 公式HP、準拠州HP(登記情報)。補強として証取HPの公式ページも保存。
  • 成果物: 創業年・設立州などの構造化JSON+参照URLリスト+PDF/HTMLの証拠ファイル。

なぜLLMだけでは足りないか

ChatGPTにもChatGPT Searchという機能がありますが、「公式サイトから情報を取ってきて」と頼むだけでは、引用にまとめサイトや孫引きが紛れたり、検索クエリが弱く適切な公式サイトに届かなかったりします。米国企業の場合、一次情報の主な参照先は「州HP(たとえばデラウェア州の法人データベースの検索ページなど、フォーム入力が必須のサイト)」や「証券取引所の公式ページ(NASDAQ/NYSEなど、JSでレンダリングされる株価ページ)」ですが、これらは単純なHTTPリクエストでは内容が取得できず、ブラウザで手順を踏む必要があります。そこで、まず到達経路をルールで固めたうえで、LLMは「どれが公式か」「何を抽出するか」の判断と抽出に専念させるハイブリッド構成にしました。

進め方の設計(決まったルート+証拠付き)

  1. クエリ生成と公式ドメインのあたり付け
    LLMに複数クエリを出させ、上位結果からドメインを切り出し、LLMに「公式らしさ」「企業名との一致度」「第三者サイトの除外」を評価させてスコアリングします。誤ってまとめサイトを拾わないよう、除外条件と採択基準をプロンプトで明記したのが効きました。
    例として、こんな感じのプロンプトを使いました。
与えられた検索結果リストから、企業の公式サイトである可能性が高いドメインを1つ選んでください。
優先: 企業名と一致/近似するドメイン、短いトップレベルドメイン(.com等)、公式トップページに近いパス。
除外: wikipedia, linkedin, facebook, amazon, newsサイト, 求人サイト, ECサイト, まとめサイト。
出力: {"domain": "...", "reason": "..."} のJSON形式。

(検索結果リスト)
  1. ドメイン固定で再探索し、社内ページを洗う
    ドメインを確定させたら、その範囲で「どこを深く掘るか」を決めます。三方向でURLを集めるのがポイントです。
    • 方向1: site: 検索(検索エンジンに「このドメイン内だけで検索して」と指示するクエリ)で「history」「ir」「about us」「company profile」など沿革・IR系のキーワードを投げ、インデックスに載っているページを拾います。
    • 方向2: sitemap.xml(サイト側が公開するURL一覧ファイル)を探します。/sitemap.xml だけでなく、/sitemap_index.xml/sitemaps/sitemap.xml など複数パス候補を順に試し、一覧からURLを一括で抜きます。
    • 方向3: トップページから内部リンクをたどり、「about/history/ir/company」のような文字列を含むパスを優先度付きでクロールします。
      これらを統合することで、検索エンジンのランキングやクローラビリティに依存せず、公式ドメイン内で一次情報が埋まっているページを高確率で拾いやすくなります。

  1. 手続きが必要な場所はブラウザ操作で踏み抜く
    州HPの検索フォームやJS依存の証取HPページは、HTTPリクエストでは空振りします。ここは人間がやるのと同じ手順を自動化し、ヘッドレスブラウザでフォーム入力・クリックまで行い、レンダリング後の状態をPDF/HTMLで保存しました。クッキー同意バナーなども簡易的にハンドリングして、止まらないようにしています。
    今回の問題設定では、特定のページに対して処理を固定できたため、ブラウザ操作の手順もルール化しやすかったです。一方、不特定のSPAサイトを参照する必要がある場合は、より抽象化してLLMに手順を生成させるアプローチも考えられます。

  1. LLMで抽出し、複数候補をマージ
    候補HTMLをLLMに渡し、スキーマを指定して構造化抽出します。素のHTMLに情報が少ない場合もあるため、フォールバックとして印刷pdfをOCRでテキスト化したものをLLMに渡すこともできます。
    創業年のように「創業年≠登記日」の誤読が起きやすい項目は、優先順位と禁止事項をプロンプトに明記しました。複数候補が出た場合は、LLMに統合してもらい、一貫した値を決めるステップを入れました。
    例として、こんな感じのプロンプトを使いました。
あなたは企業の登記情報に詳しいアナリストです。以下のHTMLテキストから「法人としての設立年(incorporation year)」を1つだけ抽出してください。
優先: 登記年や設立年を明示した文脈、沿革の年表にある法人設立の記述。
禁止: 単なる創業年や上場年、製品発売年だけを採用しない。曖昧な年は採らない。
出力: {"incorporation_year": 2000, "reason": "本文の記述を要約して根拠を示す"} のJSON形式。

(HTMLテキスト)

"incorporation year"はnull許容にして、見つからなければ空にできるようにしました。
このようなプロンプトでHTMLチャンクごとに抽出し、最後にマージステップで整合を取ることで、誤読を減らしつつ信頼度の高い値を得られました。

  1. 証拠とトレーサビリティを残す
    証拠となるものとして、参照サイトの印刷pdfや、参照元となった具体的なHTMLの記述をリンクにして保存するなどが考えられます。参照URLリスト、PDF/HTMLのパスを結果JSONに同梱し、「どこを見たか」を後から辿れるようにしました。途中で失敗しても、取得できた部分は残す設計にして、リランや人手チェックを容易にしました。ログには使用したLLMエンジンや到達URLも記録し、トレースしやすくしています。
    具体的には、JSON出力に found_urls(踏んだURL群)、source_url(根拠ページ)、official_website_url(採択ドメイン)、error_message(失敗理由)を入れておき、ブラウザで取得した証拠PDF/HTMLは日時付きで保存する運用にしました。Logfireのような分散トレーシングで、ステップごとのスパン名を揃えておくと、どこで止まったかが一目で分かります。

    出力結果のjson例
    証拠として保存されたwebページ印刷pdf
    証拠として保存されたwebページ印刷pdf例

実装で入れた工夫

  • 検索と公式判定の手当て: 除外キーワード(wikipedia, linkedin など)をクエリに含め、不要なドメインを減らしています。
  • サイト内探索のヒューリスティクス: site: 検索とsitemap候補を併用し、トップページからの内部リンクも「about/history/ir」などのキーワードで優先度付けするようなプロンプトを設定しました。結果が薄いときはIR系のキーワードを足したクエリを追加で回しています。
  • ブラウザ取得の安定化: ヘッドレスブラウザでクッキー同意バナーを自動クリックし、特定要素が出るまで待機してから DevTools の printToPDF でエビデンス保存しました。フォーム入力も自動化し、HTTPアクセスでは見えない情報を確実に取るようにしています。
  • 抽出とマージのプロンプト設計: 創業年と登記日のニュアンスずれの問題があったのですが、創業年と登記日の優先順位、使ってはいけない根拠(単なる上場年など)を明示しました。本文を優先チャンク+通常チャンクに分け、LLMの構造化出力で揺れを抑え、複数候補は信頼度付きで統合しています。
  • エビデンスと再実行性: JSONに参照URLリストとPDF/HTMLのパスを格納し、途中で失敗しても残った部分を後から再利用できるようにしました。ログには使用エンジンや到達URLも記録してトレースしやすくしています。

Agent実装の抜粋とトレース例

エージェントの中核は「会社名取得→公式サイト推定→ページ収集→抽出→証拠保存→JSON書き出し」という6ステップで、各ステップを分散トレーシングのスパンで囲み、LLMが絡む部分とブラウザ操作部分を分けて監視しています。

# 疑似コード: 創業年エージェントの主要フロー
with span("founding_year_extraction", ticker=ticker):
    company = fetch_company_name(ticker)                 # step_1
    official = find_official_domain(company)             # step_2
    pages = collect_candidate_pages(official)            # step_3 (site検索 + sitemap + 内部リンク)
    year, html, url = extract_year_with_llm(pages)       # step_4
    save_evidence_pdf_and_html(html, url, ticker)        # step_5
    save_result_json(ticker, company, official, year, url, pages)  # step_6

公式ドメインを選ぶステップでは、検索結果をLLMに評価させ、構造化出力でURLを返す形にしました。曖昧なドメインやSNS/まとめサイトを除外し、理由付きで返させることで後からのトレースもしやすくしています。

# 疑似コード: 公式ドメイン選定(LLM structured output)
def find_official_domain(company: str) -> str:
    query = f'"{company}" official site -wikipedia -linkedin -facebook -amazon'
    candidates = search_web(query)[:5]  # (url, title, snippet) のリスト

    prompt = f"""
あなたは企業サイト判定の専門家です。
候補URLの中から、その企業の公式トップページに最も近いものを1つ選んでください。
優先: 企業名と一致/近似するドメイン、短いトップレベルドメイン(.com等)、トップページに近いパス。
除外: wikipedia, linkedin, facebook, 求人/ニュース/EC/まとめサイト、/news /blog /careers のような深いパス。
出力: {{ "url": "...", "reason": "..." }} のJSONのみ。

候補:
{format_candidates(candidates)}
"""

    answer = llm_chat(
        prompt=prompt,
        schema={"url": "string", "reason": "string"},  # structured output
        temperature=0
    )
    if not answer["url"].startswith("http"):
        raise ValueError("LLM returned invalid URL")
    return answer["url"]

成功・失敗を問わずJSONを残し、found_urls に踏んだURL群、source_url に根拠ページ、error_message に失敗理由を入れる設計にしたことで、後から「どこで止まったか」を再現しやすくしています。ブラウザで取った証拠PDF/HTMLは日時付きで保存し、記事中には代表例だけ載せる形にしました。

つまずきと対処

  • 検索の揺れ: 公式ドメインがサブドメインで分かれるケースが多く、一発で当たりませんでした。ドメインスコアリングをLLMに任せ、複数候補を保持して再検索することで改善しました。
  • フォーム依存・JS重めページ: HTTPリクエストでは空振りするので、潔くブラウザ操作に寄せました。PDF/HTMLとして証拠保存すると、後段の抽出失敗時も手元で検証できます。
  • 抽出の取り違え: 創業年と登記日を混同しがちでした。プロンプトで優先順位を明示し、複数候補があればマージステップで整合を取るようにしました。
  • 途中失敗時の空振り: 途中でこけてもURLリストと部分結果を残し、後から手動・自動で再実行できるようにしました。

まとめ

米国株の一次情報を証拠付きで集める業務に対し、ルールで到達経路の範囲を絞り、LLMで判断と抽出を補うハイブリッドを組むことで、信頼できるドメインから必要情報を拾う作業をLLM込みで自動化しやすくなりました。検索のあたり付け→公式ドメイン特定→サイト内探索→ブラウザ操作→LLM抽出→証拠束ね、という筋を押さえれば、別分野のタスクでも再利用しやすいと思います。実務で「一次情報+証拠」が求められる場面の参考になれば幸いです。

ChatGPT Atlas

LLMを組み込んだワークフローではなく、WebUI自体をLLMでいじるという方針でweb上の情報を探索する方法として、ユーザーレベルであればChatGPT Atlasがあると思います。本記事のタスクに取り組んでいた時点(2025年夏)では簡単に使えるWebUIエージェントが少なかったですが、半年もしないうちに出てきました。この記事を書くにあたって、同じタスクをChatGPT Atlasのエージェントモードに全任せでやってみましたが、高い精度で問題なくタスク遂行ができていました。割としっかり目にプロンプトで制御する必要はあると思いますが、今から取り組むなら、わざわざルールベース含むLLMワークフローを固める必要はないかもしれません。

Finatext Tech Blog

Discussion