From d5df5b8283b0e7697b6f829836a157dbe7a44331 Mon Sep 17 00:00:00 2001 From: KiriAky 107 Date: Thu, 9 Apr 2026 21:00:31 +0800 Subject: [PATCH] =?UTF-8?q?=E5=A2=9E=E5=BC=BA=E6=A8=A1=E6=9D=BF=E5=A1=AB?= =?UTF-8?q?=E5=85=85=E6=9C=8D=E5=8A=A1=E6=94=AF=E6=8C=81=E9=9D=9E=E7=BB=93?= =?UTF-8?q?=E6=9E=84=E5=8C=96=E6=96=87=E6=A1=A3AI=E5=88=86=E6=9E=90?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 引入markdown_ai_service服务支持Markdown文档处理 - 实现_nonstructured_docs_for_fields方法对非结构化文档进行AI分析 - 优化LLM提示词,改进数据提取的准确性和格式规范 - 支持从Markdown表格格式{tables: [{headers: [...], rows: [...]}]}中提取数据 - 添加文档章节结构解析,提升上下文理解能力 - 增加JSON响应格式修复功能,提高数据解析成功率 --- backend/app/services/template_fill_service.py | 200 +++++++++++++++++- 1 file changed, 193 insertions(+), 7 deletions(-) diff --git a/backend/app/services/template_fill_service.py b/backend/app/services/template_fill_service.py index 71976a6..dfea7f8 100644 --- a/backend/app/services/template_fill_service.py +++ b/backend/app/services/template_fill_service.py @@ -10,6 +10,7 @@ from typing import Any, Dict, List, Optional from app.core.database import mongodb from app.services.llm_service import llm_service from app.core.document_parser import ParserFactory +from app.services.markdown_ai_service import markdown_ai_service logger = logging.getLogger(__name__) @@ -233,6 +234,12 @@ class TemplateFillService: confidence=1.0 ) + # 无法直接从结构化数据提取,尝试 AI 分析非结构化文档 + ai_structured = await self._analyze_unstructured_docs_for_fields(source_docs, field, user_hint) + if ai_structured: + logger.info(f"✅ 字段 {field.name} 通过 AI 分析结构化提取到数据") + return ai_structured + # 无法从结构化数据提取,使用 LLM logger.info(f"字段 {field.name} 无法直接从结构化数据提取,使用 LLM...") @@ -244,18 +251,20 @@ class TemplateFillService: if user_hint: hint_text = f"{user_hint}。{hint_text}" - prompt = f"""你是一个专业的数据提取专家。请从以下文档内容中提取"{field.name}"字段的所有行数据。 + prompt = f"""你是一个专业的数据提取专家。请从以下文档内容中提取与"{field.name}"相关的所有信息。 -参考文档内容(已提取" {field.name}"列的数据): +提示词: {hint_text} + +文档内容: {context_text} -请提取上述所有行的" {field.name}"值,存入数组。每一行对应数组中的一个元素。 -如果某行该字段为空,请用空字符串""占位。 +请分析文档结构(可能包含表格、标题段落等),找出所有与"{field.name}"相关的数据。 +如果找到表格数据,返回多行值;如果是非表格段落,提取关键信息。 -请严格按照以下 JSON 格式输出,不要添加任何解释: +请严格按照以下 JSON 格式输出: {{ - "values": ["第1行的值", "第2行的值", "第3行的值", ...], - "source": "数据来源的文档描述", + "values": ["第1行的值", "第2行的值", ...], + "source": "数据来源描述", "confidence": 0.0到1.0之间的置信度 }} """ @@ -473,6 +482,29 @@ class TemplateFillService: elif isinstance(row, list): doc_content += " | ".join(str(cell) for cell in row) + "\n" row_count += 1 + elif doc.structured_data and doc.structured_data.get("tables"): + # Markdown 表格格式: {tables: [{headers: [...], rows: [...]}]} + tables = doc.structured_data.get("tables", []) + for table in tables: + if isinstance(table, dict): + headers = table.get("headers", []) + rows = table.get("rows", []) + if rows and headers: + doc_content += f"\n【文档: {doc.filename} - 表格】\n" + doc_content += " | ".join(str(h) for h in headers) + "\n" + for row in rows: + if isinstance(row, list): + doc_content += " | ".join(str(cell) for cell in row) + "\n" + row_count += 1 + # 如果有标题结构,也添加上下文 + if doc.structured_data.get("titles"): + titles = doc.structured_data.get("titles", []) + doc_content += f"\n【文档章节结构】\n" + for title in titles[:20]: # 限制前20个标题 + doc_content += f"{'#' * title.get('level', 1)} {title.get('text', '')}\n" + # 如果没有提取到表格内容,使用纯文本 + if not doc_content.strip(): + doc_content = doc.content[:5000] if doc.content else "" elif doc.content: doc_content = doc.content[:5000] @@ -720,6 +752,21 @@ class TemplateFillService: logger.info(f"从文档 {doc.filename} 提取到 {len(values)} 个值") break + # 处理 Markdown 表格格式: {tables: [{headers: [...], rows: [...]}]} + elif structured.get("tables"): + tables = structured.get("tables", []) + for table in tables: + if isinstance(table, dict): + headers = table.get("headers", []) + rows = table.get("rows", []) + values = self._extract_column_values(rows, headers, field_name) + if values: + all_values.extend(values) + logger.info(f"从 Markdown 表格提取到 {len(values)} 个值") + break + if all_values: + break + return all_values def _extract_column_values(self, rows: List, columns: List, field_name: str) -> List[str]: @@ -1005,6 +1052,145 @@ class TemplateFillService: content = text.strip()[:500] if text.strip() else "" return [content] if content else [] + async def _analyze_unstructured_docs_for_fields( + self, + source_docs: List[SourceDocument], + field: TemplateField, + user_hint: Optional[str] = None + ) -> Optional[FillResult]: + """ + 对非结构化文档进行 AI 分析,尝试提取结构化数据 + + 适用于 Markdown 等没有表格格式的文档,通过 AI 分析提取结构化信息 + + Args: + source_docs: 源文档列表 + field: 字段定义 + user_hint: 用户提示 + + Returns: + FillResult 如果提取成功,否则返回 None + """ + # 找出非结构化的 Markdown/TXT 文档(没有表格的) + unstructured_docs = [] + for doc in source_docs: + if doc.doc_type in ["md", "txt", "markdown"]: + # 检查是否有表格 + has_tables = ( + doc.structured_data and + doc.structured_data.get("tables") and + len(doc.structured_data.get("tables", [])) > 0 + ) + if not has_tables: + unstructured_docs.append(doc) + + if not unstructured_docs: + return None + + logger.info(f"发现 {len(unstructured_docs)} 个非结构化文档,尝试 AI 分析...") + + # 对每个非结构化文档进行 AI 分析 + for doc in unstructured_docs: + try: + # 使用 markdown_ai_service 的 statistics 分析类型 + # 这种类型专门用于政府统计公报等包含数据的文档 + hint_text = field.hint if field.hint else f"请提取{field.name}的信息" + if user_hint: + hint_text = f"{user_hint}。{hint_text}" + + # 构建针对字段提取的提示词 + prompt = f"""你是一个专业的数据提取专家。请从以下文档内容中提取与"{field.name}"相关的所有数据。 + +字段提示: {hint_text} + +文档内容: +{doc.content[:8000] if doc.content else ""} + +请完成以下任务: +1. 仔细阅读文档,找出所有与"{field.name}"相关的数据 +2. 如果文档中有表格数据,提取表格中的对应列值 +3. 如果文档中是段落描述,提取其中的关键数值或结论 +4. 返回提取的所有值(可能多个,用数组存储) + +请用严格的 JSON 格式返回: +{{ + "values": ["值1", "值2", ...], + "source": "数据来源说明", + "confidence": 0.0到1.0之间的置信度 +}} + +如果没有找到相关数据,返回空数组 values: []""" + + messages = [ + {"role": "system", "content": "你是一个专业的数据提取助手,擅长从政府统计公报等文档中提取数据。请严格按JSON格式输出。"}, + {"role": "user", "content": prompt} + ] + + response = await self.llm.chat( + messages=messages, + temperature=0.1, + max_tokens=5000 + ) + + content = self.llm.extract_message_content(response) + logger.info(f"AI 分析返回: {content[:500]}") + + # 解析 JSON + import json + import re + + # 清理 markdown 格式 + cleaned = content.strip() + cleaned = re.sub(r'^```json\s*', '', cleaned, flags=re.MULTILINE) + cleaned = re.sub(r'^```\s*', '', cleaned, flags=re.MULTILINE) + cleaned = cleaned.strip() + + # 查找 JSON + json_start = -1 + for i, c in enumerate(cleaned): + if c == '{' or c == '[': + json_start = i + break + + if json_start == -1: + continue + + json_text = cleaned[json_start:] + try: + result = json.loads(json_text) + values = self._extract_values_from_json(result) + if values: + return FillResult( + field=field.name, + values=values, + value=values[0] if values else "", + source=f"AI分析: {doc.filename}", + confidence=result.get("confidence", 0.8) + ) + except json.JSONDecodeError: + # 尝试修复 JSON + fixed = self._fix_json(json_text) + if fixed: + try: + result = json.loads(fixed) + values = self._extract_values_from_json(result) + if values: + return FillResult( + field=field.name, + values=values, + value=values[0] if values else "", + source=f"AI分析: {doc.filename}", + confidence=result.get("confidence", 0.8) + ) + except json.JSONDecodeError: + pass + + except Exception as e: + logger.warning(f"AI 分析文档 {doc.filename} 失败: {str(e)}") + continue + + return None + # ==================== 全局单例 ====================