fix: 智能填单地址解析错误和产品名带型号

Prompt 改进:
- 明确 customer_address 只含地理地址,禁止混入产品描述/手机号/订单号
- 明确 product_name 只填材料类型不带型号编号
- specification 必须包含厚度/颜色/尺寸等完整参数

后处理兜底:
- 新增 _clean_address 检测地址中混入的非地址内容并重新提取
- 新增 _extract_address_after_phone 从手机号后提取纯地理地址
- 去除地址中的订单号(SYY+5681 等)

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
taiyi 2026-06-04 15:59:05 +08:00
parent d9952cdc25
commit 7351de6a8a

View File

@ -312,21 +312,35 @@ class LLMOrderParser:
TEXT_SYSTEM_PROMPT = """你是高温布/特氟龙/胶带/输送带行业的订单解析助手。从业务员粘贴的文本中提取订单信息。
## 输入格式
业务员粘贴的文本通常是两列结构tab 或多个空格分隔
## 输入格式(非常重要)
业务员粘贴的文本由两列组成中间tab 或多个空格分隔
- 左列产品描述厚度 + 材料名 + 尺寸 + 数量 + 工艺要求
- 右列客户姓名手机号地址备注
同一客户可能有多个产品行每行一个产品
示例0.13mm 咖色高温布 1*10 SYY+568113711561796广东省广州市白云区石井街道
## 产品名称识别
从描述中提取材料类型作为产品名称去除厚度/颜色前缀
- "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+568113711561796广东省广州市白云区
输出: {{"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+568113711561796广东省 广州市 白云区 石井街道
输出: {{"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 识别图片并进行版面分析。