diff --git a/backend/app/services/ai_service.py b/backend/app/services/ai_service.py index d82ac44..8371aa9 100644 --- a/backend/app/services/ai_service.py +++ b/backend/app/services/ai_service.py @@ -312,21 +312,35 @@ class LLMOrderParser: TEXT_SYSTEM_PROMPT = """你是高温布/特氟龙/胶带/输送带行业的订单解析助手。从业务员粘贴的文本中提取订单信息。 -## 输入格式 -业务员粘贴的文本通常是两列结构,用 tab 或多个空格分隔: +## 输入格式(非常重要) +业务员粘贴的文本由两列组成,中间用 tab 或多个空格分隔: - 左列:产品描述(厚度 + 材料名 + 尺寸 + 数量 + 工艺要求) - 右列:客户姓名,手机号,地址(备注) -同一客户可能有多个产品行,每行一个产品。 +示例:0.13mm 咖色高温布 1*10米 SYY+5681,13711561796,广东省广州市白云区石井街道 -## 产品名称识别 -从描述中提取材料类型作为产品名称,去除厚度/颜色前缀: -- "0.13mm 咖色高温布" → product_name="高温布",specification 包含厚度和颜色 +必须严格按这个结构拆分,不要把左列的产品信息混入右列的客户地址中。 + +## customer_address 规则(非常重要) +customer_address 只能包含地理地址信息(省/市/区/县/镇/街道/路/号/楼/室/栋/公司名),绝对不能包含: +- 产品描述(厚度、材料名、尺寸等) +- 客户姓名和手机号 +- 订单编号(如 SYY+5681) +- 整段原始文本 + +## product_name 规则(非常重要) +product_name 只填产品材料类型名称,不要带型号编号: +- "0.13mm 咖色高温布" → product_name="高温布" - "145g咖色高温布" → product_name="高温布" - "黑色4*4mm网格输送带" → product_name="网格输送带" - "咖色特氟龙胶带" → product_name="特氟龙胶带" - "用凯夫拉布做" → product_name="凯夫拉布" -常见产品:高温布、特氟龙胶带、网格输送带、烧烤网格片、凯夫拉布、硅胶布、胶带 +常见产品名:高温布、特氟龙胶带、网格输送带、烧烤网格片、凯夫拉布、硅胶布、胶带 + +## specification 规则 +specification 必须包含所有产品参数,格式为 "厚度/克重 颜色 标准化尺寸": +- "0.13mm 咖色高温布 1*10米" → specification="0.13mm 咖色 1000mm×10000mm" +- "145g咖色高温布 25*30厘米 1000张" → specification="145g 咖色 250mm×300mm" ## 尺寸标准化 将所有尺寸统一转换为 "长mm×宽mm" 格式: @@ -335,53 +349,43 @@ class LLMOrderParser: - "25*30厘米" → "250mm×300mm" - "长800mm宽250mm" → "800mm×250mm" - "80厘米宽*1米长" → "1000mm×800mm" -- "1.25m宽*50m长" → "1250mm×50000mm" ## 数量单位 常用单位:张、个、卷、条、套、片、块 ## 备注提取 -以下内容应提取到 remark 字段(订单级)或 item.remark(产品级): -- 括号中的内容:(不要浅色的)、(单片卷)、(加急安排) -- 尾部工艺要求:带回仓库包装、黑膜包边、牛鼻子接头、重叠位置要加固、按图纸生产 -- 产品级备注放入 items[].remark,订单级备注放入顶层 remark +- 括号中的内容:(不要浅色的)、(单片卷)、(加急安排)→ items[].remark +- 尾部工艺要求:带回仓库包装、黑膜包边、牛鼻子接头 → items[].remark -## 严格按以下 JSON 格式输出,不要输出其他内容: +## 严格按以下 JSON 格式输出: {{ "customer_name": "客户姓名", - "customer_mobile": "手机号", - "customer_address": "地址", + "customer_mobile": "11位手机号", + "customer_address": "仅包含地理地址", "order_source": null, "delivery_type": null, - "remark": "订单级备注(如配送要求、整体说明)", + "remark": "订单级备注", "items": [ {{ - "product_name": "产品名称(材料类型)", - "specification": "规格(标准化尺寸 + 厚度/克重等参数)", + "product_name": "产品材料类型(不带型号)", + "specification": "厚度 颜色 标准化尺寸", "unit": "数量单位", "quantity": 数量数字, "sale_price": null, - "remark": "产品级备注(工艺要求、特殊说明)" + "remark": "产品级备注" }} ] }} -## 规则 -- 手机号必须是 11 位数字,以 1 开头 -- 数量必须是数字(不是字符串) -- 无法识别的字段填 null,不要编造 -- 如果文本中有多个产品,每个产品一个 items 条目 -- 同一客户的所有产品共享同一个 customer_name/customer_mobile/customer_address - ## 示例 -输入: 0.13mm 咖色高温布 1*10米 SYY+5681,13711561796,广东省广州市白云区 -输出: {{"customer_name":"SYY+5681","customer_mobile":"13711561796","customer_address":"广东省广州市白云区","remark":null,"items":[{{"product_name":"高温布","specification":"0.13mm 咖色 1000mm×10000mm","unit":null,"quantity":null,"sale_price":null,"remark":null}}]}} +输入: 0.13mm 咖色高温布 1*10米 SYY+5681,13711561796,广东省 广州市 白云区 石井街道 +输出: {{"customer_name":"SYY+5681","customer_mobile":"13711561796","customer_address":"广东省广州市白云区石井街道","remark":null,"items":[{{"product_name":"高温布","specification":"0.13mm 咖色 1000mm×10000mm","unit":null,"quantity":null,"sale_price":null,"remark":null}}]}} -输入: 145g咖色高温布 25*30厘米 1000张 何莹,19932065916,河北省邯郸市大名县 -输出: {{"customer_name":"何莹","customer_mobile":"19932065916","customer_address":"河北省邯郸市大名县","remark":null,"items":[{{"product_name":"高温布","specification":"145g 咖色 250mm×300mm","unit":"张","quantity":1000,"sale_price":null,"remark":null}}]}} +输入: 145g咖色高温布 25*30厘米 1000张 何莹,19932065916,河北省 邯郸市 大名县 园中街瑞盾门业 +输出: {{"customer_name":"何莹","customer_mobile":"19932065916","customer_address":"河北省邯郸市大名县园中街瑞盾门业","remark":null,"items":[{{"product_name":"高温布","specification":"145g 咖色 250mm×300mm","unit":"张","quantity":1000,"sale_price":null,"remark":null}}]}} -输入: 用凯夫拉布做,18丝布带。宽度22.8cm,长度56.5,两边重叠6mm 50个(重叠位置要加固) 徐阳飞,15158849480,浙江省杭州市富阳区 -输出: {{"customer_name":"徐阳飞","customer_mobile":"15158849480","customer_address":"浙江省杭州市富阳区","remark":null,"items":[{{"product_name":"凯夫拉布","specification":"18丝布带 228mm×565mm 重叠6mm","unit":"个","quantity":50,"sale_price":null,"remark":"重叠位置要加固"}}]}} +输入: 用凯夫拉布做,18丝布带。宽度22.8cm,长度56.5,两边重叠6mm 50个(重叠位置要加固) 徐阳飞,15158849480,浙江省 杭州市 富阳区 场口镇 +输出: {{"customer_name":"徐阳飞","customer_mobile":"15158849480","customer_address":"浙江省杭州市富阳区场口镇","remark":null,"items":[{{"product_name":"凯夫拉布","specification":"18丝布带 228mm×565mm 重叠6mm","unit":"个","quantity":50,"sale_price":null,"remark":"重叠位置要加固"}}]}} 可选的产品库(名称 + 规格): {product_hints}""" @@ -1083,6 +1087,16 @@ class AIService: merged = self._build_fallback_result(rule_result) parse_source = "rule" + # 5.5 地址后处理:清理 LLM 可能混入的非地址内容 + if merged.get("customer_address"): + merged["customer_address"] = self._clean_address( + merged["customer_address"], raw_text + ) + if merged.get("customer_mobile") and not merged.get("customer_address"): + merged["customer_address"] = self._extract_address_after_phone( + raw_text, merged["customer_mobile"] + ) + # 6. 客户库预匹配 if session is not None and merged.get("customer_mobile"): try: @@ -1153,6 +1167,46 @@ class AIService: chunks.append(chunk) return chunks + _ADDRESS_KEYWORDS = {'省', '市', '区', '县', '镇', '路', '街', '号', '楼', '室', '栋', '村', '巷', '园'} + _ORDER_NO_RE = re.compile(r'[A-Z]{2,}[\d\-+]+') # SYY+5681, SO20260604 等订单号 + + def _clean_address(self, address: str, raw_text: str) -> str: + """清理 LLM 返回的地址,去除混入的非地址内容。 + + 如果地址中包含手机号或产品关键词,说明 LLM 没有正确拆分, + 此时从原始文本中重新提取。 + """ + if not address: + return address + # 如果地址中包含手机号,说明 LLM 把整段文本塞进了地址 + if self._PHONE_ANCHOR_RE.search(address): + phone_match = self._PHONE_ANCHOR_RE.search(raw_text) + if phone_match: + return self._extract_address_after_phone(raw_text, phone_match.group(0)) + return "" + # 去除订单号 + address = self._ORDER_NO_RE.sub('', address).strip() + # 去除开头的逗号/空格 + address = address.lstrip(',,、 \t') + # 如果清理后地址关键词密度太低,说明不是有效地址 + addr_hits = sum(1 for kw in self._ADDRESS_KEYWORDS if kw in address) + if addr_hits < 1 and len(address) > 5: + # 可能是 LLM 误填,尝试从原始文本提取 + phone_match = self._PHONE_ANCHOR_RE.search(raw_text) + if phone_match: + return self._extract_address_after_phone(raw_text, phone_match.group(0)) + return address + + def _extract_address_after_phone(self, text: str, phone: str) -> str: + """从原始文本中提取手机号之后的地址信息。""" + idx = text.find(phone) + if idx < 0: + return "" + after = text[idx + len(phone):].strip() + # 去掉开头的逗号/空格/顿号 + after = after.lstrip(',,、 \t') + return after if after else "" + def _ocr_and_parse_image(self, image_url: str) -> dict: """调用 OCR 识别图片并进行版面分析。