法務 AI アシスタントの構築: RAG、ガードレール、プロフェッショナル インターフェイス
2025 年の初め以来、AI によって生成されたコンテンツの 518 件が文書化されています。 幻覚を伴う症状が米国の法廷で提出された。 同期間における独立した評価によると、Westlaw AI と LexisNexis は Lexis+ — 法律界で最も人気のある 2 つのシステム — は正確な回答を生成します 特定の法律に関する質問のケースの 65 ~ 83% でのみ。問題は AI ではなく、その方法です それはどのように使用され、どのように構築されるのか。
この記事では、 法務AIアシスタント(法務副操縦士) 幻覚の問題に直接取り組む専門家: RAG on corpus サポートされていない応答をキャッチするための独自の合法的なマルチレベルのガードレール ソース、検証可能な引用、およびフローが最適化された Angular インターフェイスからの情報 弁護士の仕事のこと。
何を学ぶか
- 法的ドメイン向けの検索拡張生成 (RAG) アーキテクチャ
- 法的コーパスの構築: 規制、文章、教義
- マルチレベルのガードレール: 引用根拠、信頼度スコアリング、拒否ロジック
- 正確かつ誤解を招くことのない法的回答を提供するための迅速なエンジニアリング
- 応答ストリーミングを備えた弁護士向けの Angular インターフェイス
- システムの品質を測定するための評価フレームワーク
法的領域の RAG アーキテクチャ
一般的なチャットボットとプロの法務副操縦士の違いは次のとおりです RAG アーキテクチャでは、すべての応答は次のようにする必要があります。 特定の文書に基づいて recovered from the legal corpus, not generated by the parametric memory of the model. これは問題による幻覚を軽減するための基本的なメカニズムです 全体的なリスクは管理可能です。
from dataclasses import dataclass, field
from typing import List, Optional
from datetime import datetime
@dataclass
class LegalSource:
"""Documento sorgente recuperato per fondare la risposta."""
doc_id: str
doc_type: str # "sentenza", "legge", "regolamento", "dottrina"
title: str
citation: str # citazione formale (es. "Cass. civ. n. 12345/2024")
content_chunk: str # estratto rilevante
relevance_score: float # score di rilevanza [0, 1]
source_url: Optional[str] = None
@dataclass
class LegalQueryResult:
"""
Risultato strutturato di una query al Legal Copilot.
Ogni affermazione deve essere tracciabile a una fonte specifica.
"""
query: str
answer: str
sources: List[LegalSource]
confidence: float # confidence score complessivo [0, 1]
grounding_ratio: float # % di affermazioni supportate da fonti
uncertainty_disclaimer: str # disclaimer quando la confidenza e bassa
generated_at: datetime
model_version: str
warnings: List[str] = field(default_factory=list)
法的コーパスの構築
コーパスの品質は、法務副操縦士にとって最も重要な要素です。法的コーパス しっかりと構造化されたイタリア語には次のものが含まれている必要があります。
- 主な法律: 民法、刑法、裁判法 民事法、特別法 - 現在の (統合) バージョン
- 法学: 最高裁判所、憲法裁判所の判決、 TAR および国務院、EU 司法裁判所 (ECHR)
- 規制と回覧: イタリア銀行、Consob、プライバシー保証人、 AGCM、INPS
- 更新された原則: 権威ある法律雑誌の記事
import asyncio
import aiohttp
from bs4 import BeautifulSoup
from dataclasses import dataclass
from typing import List, AsyncIterator
import re
@dataclass
class RawLegalDocument:
source_id: str
doc_type: str
raw_text: str
metadata: dict
class LegalCorpusBuilder:
"""
Builder per il corpus giuridico.
Scarica e normalizza documenti da fonti ufficiali.
"""
# Normativa italiana via Normattiva (fonte ufficiale)
NORMATTIVA_API = "https://www.normattiva.it/uri-res/N2Ls"
async def fetch_normativa(self, uri: str) -> Optional[RawLegalDocument]:
"""
Scarica una norma da Normattiva (il portale ufficiale delle leggi italiane).
URI format: urn:nir:stato:legge:2023-12-31;234
"""
async with aiohttp.ClientSession() as session:
try:
async with session.get(
f"{self.NORMATTIVA_API}?urn={uri}&mimetype=text/plain",
timeout=aiohttp.ClientTimeout(total=30)
) as resp:
if resp.status == 200:
text = await resp.text()
return RawLegalDocument(
source_id=uri,
doc_type="legge",
raw_text=self._clean_normativa_text(text),
metadata={'source': 'normattiva', 'uri': uri}
)
except Exception as e:
print(f"Errore fetch {uri}: {e}")
return None
def _clean_normativa_text(self, text: str) -> str:
"""Rimuove markup e normalizza il testo normativo."""
# Rimuovi intestazioni burocratiche
text = re.sub(r'^.*?CAPO I', 'CAPO I', text, flags=re.DOTALL)
# Normalizza a capo
text = re.sub(r'\n{3,}', '\n\n', text)
# Rimuovi numeri di pagina
text = re.sub(r'\n\d+\n', '\n', text)
return text.strip()
def chunk_legal_text(
self,
doc: RawLegalDocument,
max_chars: int = 1500,
overlap_chars: int = 200
) -> List[dict]:
"""
Chunking strutturato per testi normativi.
Divide per articoli mantenendo la struttura legislativa.
"""
chunks = []
# Pattern per articoli: "Art. 1" o "Articolo 1" con varianti
article_pattern = re.compile(
r'(?:Art(?:icolo)?\.?\s+(\d+(?:\s*-\s*(?:bis|ter|quater|quinquies))?)'
r'|(\d+\.\s+))',
re.IGNORECASE
)
articles = list(article_pattern.finditer(doc.raw_text))
if not articles:
# Nessuna struttura: chunk fisso con overlap
for i in range(0, len(doc.raw_text), max_chars - overlap_chars):
chunk_text = doc.raw_text[i:i + max_chars]
chunks.append({
'content': chunk_text,
'doc_id': doc.source_id,
'doc_type': doc.doc_type,
'metadata': doc.metadata
})
else:
for idx, match in enumerate(articles):
start = match.start()
end = articles[idx + 1].start() if idx + 1 < len(articles) else len(doc.raw_text)
chunk_text = doc.raw_text[start:end].strip()
if len(chunk_text) > max_chars:
# Articolo molto lungo: suddividi ulteriormente
for j in range(0, len(chunk_text), max_chars - overlap_chars):
chunks.append({
'content': chunk_text[j:j + max_chars],
'doc_id': doc.source_id,
'article_ref': match.group(0),
'doc_type': doc.doc_type,
'metadata': doc.metadata
})
else:
chunks.append({
'content': chunk_text,
'doc_id': doc.source_id,
'article_ref': match.group(0),
'doc_type': doc.doc_type,
'metadata': doc.metadata
})
return chunks
マルチレベルガードレールを備えた RAG システム
法務副操縦士とガードレールを備えた RAG システムの中心: すべての質問が必要なわけではありません 応答を受け取ります。取得した情報源が質問を十分にカバーしていない場合は、 システムは、推測的な応答を生成するのではなく、これを明示的に示す必要があります。
from langchain_openai import ChatOpenAI
from langchain.schema import SystemMessage, HumanMessage
from sentence_transformers import SentenceTransformer, util
import json
import re
from typing import List, Tuple
class LegalGuardrailSystem:
"""
Sistema di guardrail multi-livello per il Legal Copilot.
Impedisce la generazione di risposte non supportate dalle fonti.
"""
# Prompt di sistema con istruzioni esplicite anti-hallucination
SYSTEM_PROMPT = """Sei un assistente legale AI altamente specializzato.
REGOLE ASSOLUTE:
1. Rispondi SOLO basandoti sui documenti forniti nel contesto.
2. Se le fonti non coprono adeguatamente la domanda, di' esplicitamente
"Le fonti disponibili non sono sufficienti per rispondere a questa domanda."
3. Cita sempre la fonte specifica per ogni affermazione (art. X, sentenza Y).
4. Non interpretare o speculare oltre quanto dichiarato nelle fonti.
5. Usa linguaggio giuridico preciso, non parafrasare formule legali standard.
6. Segnala quando una norma potrebbe essere stata modificata di recente.
FORMATO RISPOSTA:
- Risposta strutturata con paragrafi
- Ogni affermazione seguita da [Fonte: ...]
- Conclusione con disclaimer se appropriato"""
def __init__(self, llm_model: str = "gpt-4o", embedding_model: str = "nlpaueb/legal-bert-base-uncased"):
self.llm = ChatOpenAI(model=llm_model, temperature=0.1, max_tokens=2000)
self.embedding_model = SentenceTransformer(embedding_model)
def _compute_grounding_score(
self,
answer: str,
sources: List[LegalSource]
) -> Tuple[float, List[str]]:
"""
Calcola quanto la risposta e 'fondata' nelle sorgenti.
Usa similarity semantica tra frasi della risposta e chunk delle fonti.
"""
if not sources:
return 0.0, ["Nessuna fonte disponibile"]
# Estrai frasi dalla risposta
sentences = [s.strip() for s in re.split(r'[.!?]', answer) if len(s.strip()) > 20]
if not sentences:
return 0.0, []
source_texts = [s.content_chunk for s in sources]
ungrounded = []
sentence_embeds = self.embedding_model.encode(sentences, convert_to_tensor=True)
source_embeds = self.embedding_model.encode(source_texts, convert_to_tensor=True)
grounded_count = 0
for i, sent_embed in enumerate(sentence_embeds):
max_sim = float(util.cos_sim(sent_embed, source_embeds).max())
if max_sim >= 0.65:
grounded_count += 1
else:
ungrounded.append(sentences[i])
grounding_ratio = grounded_count / len(sentences) if sentences else 0.0
return grounding_ratio, ungrounded
def _check_refusal_conditions(self, query: str, sources: List[LegalSource]) -> Optional[str]:
"""
Verifica se il sistema deve rifiutare di rispondere.
Restituisce il motivo del rifiuto o None se si può procedere.
"""
# Nessuna fonte trovata
if not sources:
return "Non ho trovato documenti rilevanti nel corpus per rispondere a questa domanda."
# Tutte le fonti hanno rilevanza molto bassa
max_relevance = max(s.relevance_score for s in sources)
if max_relevance < 0.4:
return (
f"Le fonti disponibili hanno una rilevanza troppo bassa (max: {max_relevance:.2f}) "
"per rispondere con sufficiente affidabilità."
)
# Query su consulenza legale personale specifica
advice_patterns = [
r'cosa devo fare (io|noi)', r'ho torto o ragione',
r'posso vincere la causa', r'devo (firmare|accettare|rifiutare)'
]
for pattern in advice_patterns:
if re.search(pattern, query, re.IGNORECASE):
return (
"Non posso fornire consulenza legale personalizzata. "
"Rivolgiti a un avvocato abilitato per il tuo caso specifico."
)
return None # Nessun rifiuto: procedi
async def generate_legal_answer(
self,
query: str,
retrieved_sources: List[LegalSource]
) -> LegalQueryResult:
"""
Genera una risposta legale fondata sulle sorgenti.
"""
from datetime import datetime
# Step 1: Verifica condizioni di rifiuto
refusal_reason = self._check_refusal_conditions(query, retrieved_sources)
if refusal_reason:
return LegalQueryResult(
query=query,
answer=refusal_reason,
sources=[],
confidence=0.0,
grounding_ratio=0.0,
uncertainty_disclaimer=refusal_reason,
generated_at=datetime.utcnow(),
model_version="gpt-4o-guardrailed-v1",
warnings=["RIFIUTO: " + refusal_reason]
)
# Step 2: Costruisci contesto dalle fonti
context = "\n\n---\n\n".join([
f"[{s.doc_type.upper()}] {s.citation}\n{s.content_chunk}"
for s in retrieved_sources
])
messages = [
SystemMessage(content=self.SYSTEM_PROMPT),
HumanMessage(content=f"CONTESTO NORMATIVO:\n{context}\n\nDOMANDA: {query}")
]
# Step 3: Genera risposta con LLM
response = await self.llm.ainvoke(messages)
answer = response.content
# Step 4: Calcola grounding score
grounding_ratio, ungrounded = self._compute_grounding_score(answer, retrieved_sources)
# Step 5: Genera disclaimer se grounding insufficiente
disclaimer = ""
warnings = []
if grounding_ratio < 0.7:
disclaimer = (
f"ATTENZIONE: Il {(1-grounding_ratio)*100:.0f}% delle affermazioni potrebbe "
"non essere direttamente supportato dalle fonti citate. Verificare sempre "
"con il testo normativo originale."
)
warnings.append(f"Grounding score basso: {grounding_ratio:.2%}")
confidence = (
grounding_ratio * 0.6 +
(max(s.relevance_score for s in retrieved_sources) if retrieved_sources else 0) * 0.4
)
return LegalQueryResult(
query=query,
answer=answer,
sources=retrieved_sources,
confidence=confidence,
grounding_ratio=grounding_ratio,
uncertainty_disclaimer=disclaimer,
generated_at=datetime.utcnow(),
model_version="gpt-4o-guardrailed-v1",
warnings=warnings
)
ストリーミングを使用したAngularインターフェース
プロの法務副操縦士は、弁護士に最適なユーザー エクスペリエンスを提供する必要があります そしてパラリーガル。インターフェースはソースをリアルタイムで表示し、拡張できるようにする必要があります。 それぞれの規範を抽出し、応答の信頼レベルを明確にします。
// legal-copilot.service.ts
import { Injectable, inject, signal } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { Observable, Subject } from 'rxjs';
export interface LegalSource {
docId: string;
docType: string;
citation: string;
contentChunk: string;
relevanceScore: number;
sourceUrl?: string;
}
export interface CopilotResponse {
query: string;
answer: string;
sources: LegalSource[];
confidence: number;
groundingRatio: number;
uncertaintyDisclaimer: string;
warnings: string[];
generatedAt: string;
}
@Injectable({ providedIn: 'root' })
export class LegalCopilotService {
private http = inject(HttpClient);
private readonly API_BASE = '/api/v1/legal-copilot';
isLoading = signal(false);
currentResponse = signal<CopilotResponse | null>(null);
streamingText = signal('');
ask(query: string): Observable<CopilotResponse> {
this.isLoading.set(true);
this.streamingText.set('');
return new Observable(observer => {
// SSE per streaming della risposta
const eventSource = new EventSource(
`{this.API_BASE}/stream?query={encodeURIComponent(query)}`
);
eventSource.addEventListener('token', (e: MessageEvent) => {
this.streamingText.update(prev => prev + e.data);
});
eventSource.addEventListener('complete', (e: MessageEvent) => {
const result = JSON.parse(e.data) as CopilotResponse;
this.currentResponse.set(result);
this.isLoading.set(false);
observer.next(result);
observer.complete();
eventSource.close();
});
eventSource.addEventListener('error', () => {
this.isLoading.set(false);
observer.error(new Error('Errore nella comunicazione con il Legal Copilot'));
eventSource.close();
});
return () => eventSource.close();
});
}
}
評価の枠組み
法務副操縦士の品質を測定するには、法的領域に固有の指標が必要です。 ユーザーの満足度を測定するだけでは不十分です。法的な正確性を評価する必要があります。
from dataclasses import dataclass
from typing import List
import statistics
@dataclass
class EvaluationCase:
"""Un caso di valutazione con risposta attesa verificata da un esperto legale."""
query: str
reference_answer: str # risposta attesa da avvocato senior
required_citations: List[str] # citazioni che devono comparire nella risposta
forbidden_claims: List[str] # affermazioni false che non devono apparire
class LegalCopilotEvaluator:
"""
Framework di evaluation per il Legal Copilot.
Combina metriche automatiche e expert review.
"""
def evaluate_response(
self,
response: LegalQueryResult,
eval_case: EvaluationCase
) -> dict:
"""Valuta una singola risposta su più dimensioni."""
# 1. Citation recall: % delle citazioni attese presenti nella risposta
answer_lower = response.answer.lower()
found_citations = sum(
1 for cit in eval_case.required_citations
if cit.lower() in answer_lower
)
citation_recall = found_citations / len(eval_case.required_citations) if eval_case.required_citations else 1.0
# 2. Hallucination rate: % delle affermazioni false rilevate
hallucinations_found = sum(
1 for claim in eval_case.forbidden_claims
if claim.lower() in answer_lower
)
hallucination_rate = hallucinations_found / len(eval_case.forbidden_claims) if eval_case.forbidden_claims else 0.0
# 3. Refusal appropriateness (solo per query senza copertura nel corpus)
# Valutazione manuale: 1 se il rifiuto era corretto, 0 se errato
return {
'citation_recall': citation_recall,
'hallucination_rate': hallucination_rate,
'grounding_ratio': response.grounding_ratio,
'confidence': response.confidence,
'answer_length': len(response.answer),
'sources_count': len(response.sources)
}
def aggregate_evaluation(self, results: List[dict]) -> dict:
"""Aggrega i risultati di più casi di valutazione."""
return {
'avg_citation_recall': statistics.mean(r['citation_recall'] for r in results),
'avg_hallucination_rate': statistics.mean(r['hallucination_rate'] for r in results),
'avg_grounding_ratio': statistics.mean(r['grounding_ratio'] for r in results),
'avg_confidence': statistics.mean(r['confidence'] for r in results),
'total_cases': len(results)
}
導入とコンプライアンスの考慮事項
必須の免責事項とシステム制限
- これは法的なアドバイスではありません。 各答えを添付する必要があります システムは法的情報を提供するものではなく、法的情報を提供するという明示的な免責事項により、 個別の法的アドバイス。法律専門家の業務は機密扱いです 資格のある弁護士へ。
- コーパスの更新: 規制が変わります。コーパスは次のことを行う必要があります 少なくとも毎週更新され、最終日付が表示されること ユーザーに表示される更新。
- 監査のためのログ記録: すべてのクエリと応答は次のようにする必要があります。 法的監査とシステムの継続的な改善のためにログインします。
- ユーザーの責任: 契約上、 システムの応答に基づいて行われた決定に対する責任 ユーザーの弁護士が保管します。
結論
Legal AI Assistant は、単に「法的文書に接続された ChatGPT」ではありません。 特殊な RAG アーキテクチャ、ガードレールを必要とする複雑なシステムです。 幻覚用のマルチレベル、更新された法的コーパスおよびインターフェース 法務ワークフロー専用に設計されています。
業界の数字は明らかです: 適切なガードレールなしで構築されたシステム 特定の法的質問に関しては、17 ~ 33% のケースで幻覚が生じます。と この記事で紹介されているアーキテクチャ — RAG + 引用根拠 + 拒否 ロジック — この割合を大幅に削減してシステムを構築することは可能です 弁護士が自信を持って調査分析ツールとして使用できるものです。
リーガルテックとAIシリーズ
- 契約分析のための NLP: OCR から理解まで
- 電子証拠開示プラットフォームのアーキテクチャ
- 動的ルールエンジンによるコンプライアンスの自動化
- 法的合意のためのスマートコントラクト: Solidity と Vyper
- 生成 AI による法的文書の要約
- 検索エンジンの法則: ベクトル埋め込み
- Scala でのデジタル署名と文書認証
- データプライバシーとGDPRコンプライアンスシステム
- 法務 AI アシスタントの構築 - 法務副操縦士 (この記事)
- LegalTech データ統合パターン







