ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

让 AI 穿上法袍:用 TypeScript 构建 MCP Server 打通 Elasticsearch 法律法规库实战

让 AI 穿上法袍:用 TypeScript 构建 MCP Server 打通 Elasticsearch 法律法规库实战 1. 法律 AI 检索为什么总在“编法条”上翻车法律场景对 AI 的要求和写文案完全不同。你问一个通用大模型“民间借贷利率上限是多少”它可能给你一个 2020 年之前的答案甚至把已废止的司法解释当成现行有效的内容引用出来。这不是模型不聪明而是它的知识来自训练语料而法律是动态更新的——新司法解释出台、旧条款废止、地方性法规修订这些变化模型根本感知不到。更麻烦的是引用溯源。律师要的不是“法律规定应当赔偿”而是“根据《民法典》第 X 条第 X 款结合最高法某号指导案例本案应……”。通用模型给不出这种颗粒度的引用因为它没有连接到真实的法律数据库。我试过用纯 RAG 方案做法律问答把法条切块塞进向量库检索出来再让模型总结。效果能用但有几个硬伤一是检索和推理混在一起模型经常“脑补”没检索到的法条二是无法做效力校验检索到的可能是已废止版本三是案例比对时模型倾向于生成看起来合理但实际不存在的“类案”。MCPModel Context Protocol协议解决的正是这个问题。它把“检索”和“推理”拆开模型负责理解你的法律问题MCP Server 负责从 Elasticsearch 和向量库里精准取回法条原文、效力状态和关联案例。模型不再“背诵”法律而是学会“查阅”法律。这篇就带你用 TypeScript 从零搭一个法律检索 MCP Server对接 Elasticsearch 做混合检索把法条检索延迟压到毫秒级同时保证每一条引用都可溯源。适合谁看有 TypeScript 基础、想给法律 AI 应用接入真实法规库的开发者正在做智能合同审查、类案检索、合规问答的工程同学以及想理解 MCP 协议在垂直领域怎么落地的技术人。2. 前置准备TaoToken 接入与项目初始化2.1 为什么法律 MCP Server 需要 TaoTokenMCP Server 本身只负责检索真正做法律推理和引用组装的还是大模型。法律场景对模型的要求是长上下文要同时读多条法条和案例、强指令遵循必须按格式引用、低幻觉不能编法条。Claude 系列在这几点上表现稳定适合做法律推理层。TaoToken 提供 Claude 系列模型的 API 接入兼容 Anthropic 原生接口格式。你可以在模型对话页面先测试法律问答效果确认模型能正确使用检索结果后再接入 MCP Server。API Key 在控制台的 API Keys 页面生成接入文档里有完整的请求示例。2.2 项目初始化mkdir mcp-legal-server cd mcp-legal-server npm init -y npm install modelcontextprotocol/sdk elastic/elasticsearch npm install -D typescript types/node tsx npx tsc --init --target ES2022 --module NodeNext --moduleResolution NodeNext --outDir disttsconfig.json关键配置确认{ compilerOptions: { target: ES2022, module: NodeNext, moduleResolution: NodeNext, outDir: dist, strict: true, esModuleInterop: true, skipLibCheck: true }, include: [src/**/*.ts] }package.json加上type: module否则 ESM 导入会报错。2.3 Elasticsearch 索引映射设计法律数据的索引映射和普通文档不同需要同时支持关键词精确匹配法条编号、文号和向量语义检索争议焦点描述。下面是一个兼顾两者的 mapping{ mappings: { properties: { law_name: { type: keyword }, article_number: { type: keyword }, official_doc_no: { type: keyword }, content: { type: text, analyzer: ik_max_word, fields: { raw: { type: keyword } } }, status: { type: keyword }, effective_date: { type: date }, abolish_date: { type: date }, related_articles: { type: keyword }, content_vector: { type: dense_vector, dims: 1024, index: true, similarity: cosine } } } }status字段存“现行有效”“已废止”“已修订”检索时作为 filter 条件。content_vector存法条的语义向量用于争议焦点的模糊匹配。related_articles存关联法条编号方便做跨法条引用链。3. 可复制配置MCP Server 核心代码骨架3.1 Server 初始化与工具定义import { Server } from modelcontextprotocol/sdk/server/index.js; import { StdioServerTransport } from modelcontextprotocol/sdk/server/stdio.js; import { ListToolsRequestSchema, CallToolRequestSchema, } from modelcontextprotocol/sdk/types.js; import { Client } from elastic/elasticsearch; const esClient new Client({ node: process.env.ES_NODE || http://localhost:9200, auth: process.env.ES_API_KEY ? { apiKey: process.env.ES_API_KEY } : undefined, }); const server new Server( { name: legal-intelligence-server, version: 1.0.0 }, { capabilities: { tools: {}, resources: {} } } );3.2 法条检索工具混合检索 效力过滤server.setRequestHandler(ListToolsRequestSchema, async () ({ tools: [ { name: query_statutes, description: 在法律库中检索法条。支持关键词和语义混合检索返回法条原文、文号、效力状态和关联条款。, inputSchema: { type: object, properties: { legal_issue: { type: string, description: 法律争议点或关键词如违约金过高调整标准, }, effective_only: { type: boolean, description: 是否仅返回现行有效法条, default: true, }, incident_date: { type: string, description: 案发日期YYYY-MM-DD用于匹配当时有效的法条版本, }, top_k: { type: number, default: 5 }, }, required: [legal_issue], }, }, ], }));3.3 执行逻辑向量 关键词混合检索server.setRequestHandler(CallToolRequestSchema, async (request) { const { name, arguments: args } request.params; if (name ! query_statutes) { throw new Error(Unknown tool: ${name}); } const legalIssue args?.legal_issue as string; const effectiveOnly args?.effective_only ! false; const incidentDate args?.incident_date as string | undefined; const topK (args?.top_k as number) || 5; // 生成查询向量实际项目调用 embedding 服务 const queryVector await embedText(legalIssue); const filters: any[] []; if (effectiveOnly) { filters.push({ term: { status: 现行有效 } }); } if (incidentDate) { filters.push({ range: { effective_date: { lte: incidentDate } } }); filters.push({ bool: { should: [ { range: { abolish_date: { gte: incidentDate } } }, { bool: { must_not: { exists: { field: abolish_date } } } }, ], }, }); } const result await esClient.search({ index: laws_v2, size: topK, query: { bool: { should: [ { knn: { field: content_vector, query_vector: queryVector, k: topK * 2, num_candidates: 100, }, }, { match: { content: { query: legalIssue, boost: 0.4 }, }, }, ], filter: filters, }, }, _source: [ law_name, article_number, content, official_doc_no, status, effective_date, related_articles, ], }); const hits result.hits.hits.map((h: any) ({ law: h._source.law_name, article: h._source.article_number, content: h._source.content, docNo: h._source.official_doc_no, status: h._source.status, effectiveDate: h._source.effective_date, related: h._source.related_articles, score: h._score, })); return { content: [ { type: text, text: JSON.stringify( { count: hits.length, results: hits }, null, 2 ), }, ], }; });3.4 案例比对工具{ name: compare_precedents, description: 比对多个判例的裁判要旨识别支持点与冲突点。, inputSchema: { type: object, properties: { precedent_ids: { type: array, items: { type: string }, description: 待比对的判例 ID 列表, }, focus_point: { type: string, description: 比对焦点如违约金调整比例, }, }, required: [precedent_ids, focus_point], }, }执行时从案例索引中按 ID 批量取回裁判要旨按focus_point做语义相似度排序返回结构化对比矩阵。3.5 启动入口const transport new StdioServerTransport(); await server.connect(transport); console.error(Legal MCP Server running on stdio);注意日志走console.errorstdout留给 MCP 协议通信否则会污染消息流。4. 验证请求与成功结果4.1 本地启动与 MCP 客户端配置npx tsx src/index.ts在 Claude Desktop 或支持 MCP 的客户端配置文件中加入{ mcpServers: { legal-intelligence: { command: npx, args: [tsx, /path/to/mcp-legal-server/src/index.ts], env: { ES_NODE: http://localhost:9200 } } } }4.2 检索延迟测试写一个简单的压测脚本连续调用 100 次query_statutesconst times: number[] []; for (let i 0; i 100; i) { const start performance.now(); await callTool(query_statutes, { legal_issue: 违约金过高调整 }); times.push(performance.now() - start); } const avg times.reduce((a, b) a b, 0) / times.length; const p99 times.sort((a, b) a - b)[98]; console.log(avg: ${avg.toFixed(1)}ms, p99: ${p99.toFixed(1)}ms);在本地 Elasticsearch 单节点、10 万条法条数据下混合检索平均延迟在 40-80msp99 在 150ms 以内。如果开启knn的num_candidates过大延迟会明显上升建议控制在 100-200。4.3 引用准确率验证准备 20 个法律问题每个问题标注正确答案的法条编号。让模型通过 MCP Server 检索后回答检查引用是否命中标注法条const testCases [ { q: 民间借贷利率司法保护上限, expected: 民间借贷司法解释第25条 }, { q: 违约金超过损失多少可调整, expected: 民法典第585条 }, // ... ]; let hit 0; for (const tc of testCases) { const result await callTool(query_statutes, { legal_issue: tc.q }); const articles JSON.parse(result).results.map((r: any) r.article); if (articles.some((a: string) tc.expected.includes(a))) hit; } console.log(引用命中率: ${(hit / testCases.length * 100).toFixed(1)}%);实测在索引映射合理、向量模型匹配的情况下Top-5 命中率能到 90% 以上。如果低于 80%优先检查向量维度和 embedding 模型是否一致。5. 本篇常见错排查5.1 MCP Server 启动后客户端无响应最常见原因是stdout被日志污染。检查代码中是否有console.log全部改成console.error。另外确认package.json里有type: module否则 ESM 导入会静默失败。5.2 Elasticsearch knn 查询报错 “field not found”content_vector字段的dims必须和 embedding 模型输出维度一致。1024 维对应 bge-large 系列1536 维对应 OpenAI text-embedding-3-small。维度不匹配时 ES 不会自动转换直接报错。5.3 检索结果包含已废止法条检查status字段的实际值是否和 filter 中的现行有效完全一致。ES 的term查询是精确匹配如果索引里存的是有效或现行filter 会失效。建议在写入时统一枚举值。5.4 模型不按格式引用法条在 MCP Server 返回的text中明确要求引用格式例如在结果前加一行说明“请按「《法律名称》第X条」格式引用并附文号”。模型对工具返回内容中的指令遵循度较高比在系统提示里写更有效。5.5 案例比对返回空结果precedent_ids对应的文档可能不在同一索引中。确认案例索引名称和法条索引分开compare_precedents工具里查的是案例索引而非laws_v2。6. 接入与调试入口法律 MCP Server 的检索层跑通后推理层建议用 Claude 系列模型做引用组装和案例比对。你可以在模型对话页面直接测试法律问答确认模型能正确使用 MCP 返回的结构化法条数据。API Key 在控制台的 API Keys 页面生成接入文档里有完整的 Anthropic 格式请求示例。如果要做长期的合同审查或类案检索 AgentCoding Plan 提供了适合持续调用的额度方案。调试阶段先用模型对话验证检索结果和引用格式确认无误后再接入生产流程。
返回列表