用户反馈很多时,逐条阅读容易遗漏共性,但让模型直接修改工单或自动决定产品优先级又过于危险。本篇把结构化输出、结果校验、证据引用和人工复核组合成一个小项目:模型只提出反馈主题,Python 负责检查编号、分组关系和置信度,最终结果仍然保留原始证据。
项目目标与边界
输入是一组带编号的用户反馈,输出是若干主题。每个主题包含名称、反馈编号、代表性证据和 0 到 1 的置信度。为了让示例保持简单,本项目规定一条反馈只归入一个主要主题;这不是普遍正确的业务规则,而是一个可以被程序检查的明确约束。
模型适合做语义归纳,例如判断“搜索结果出来得太慢”和“输入关键词后要等很久”是否属于同一类问题。模型不适合负责编号是否存在、同一条反馈是否被重复分组等确定性判断。因此要把两类职责分开:模型生成候选,代码验证契约。
先定义结果契约
先约定输出结构,再编写调用逻辑,可以避免后面拿一段自然语言去猜主题。示例结果的形状如下:
1 2 3 4 5 6 7 8 9 10
| { "themes": [ { "name": "搜索响应速度", "feedback_ids": ["F-001", "F-002"], "evidence": "用户反映搜索等待时间过长", "confidence": 0.86 } ] }
|
feedback_ids 只能引用输入中的编号,confidence 必须是 0 到 1 之间的数字,evidence 用来帮助审核者回到原始反馈核对。主题名称可以由模型生成,但不能代替证据,也不能被当成已经确认的产品结论。
最小可运行实现
下面的程序只使用 Python 标准库。demo_model 是离线演示适配器,返回固定的候选结果,不代表真实模型的运行结果;这样可以先验证校验和聚合流程,不需要网络或密钥。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91
| from dataclasses import dataclass import json
@dataclass(frozen=True) class Feedback: id: str text: str
def demo_model(rows: list[Feedback]) -> dict: """离线演示:模拟模型提出主题候选。""" return { "themes": [ { "name": "搜索响应速度", "feedback_ids": [rows[0].id, rows[1].id], "evidence": "两条反馈都描述了搜索等待时间过长", "confidence": 0.86, }, { "name": "导出格式支持", "feedback_ids": [rows[2].id], "evidence": "用户希望导出结果增加新的文件格式", "confidence": 0.72, }, { "name": "通知设置", "feedback_ids": [rows[3].id], "evidence": "用户希望能够关闭不需要的提醒", "confidence": 0.79, }, ] }
def validate(result: dict, rows: list[Feedback]) -> dict: allowed = {row.id for row in rows} themes = result.get("themes") if not isinstance(themes, list) or not themes: raise ValueError("themes 必须是非空数组")
used: set[str] = set() for theme in themes: name = theme.get("name") ids = theme.get("feedback_ids") evidence = theme.get("evidence") confidence = theme.get("confidence") if not isinstance(name, str) or not name.strip(): raise ValueError("主题必须有非空 name") if not isinstance(ids, list) or not ids: raise ValueError("feedback_ids 必须是非空数组") if not set(ids) <= allowed: raise ValueError("主题引用了不存在的反馈编号") if used.intersection(ids): raise ValueError("一条反馈不能被重复分组") if not isinstance(evidence, str) or not evidence.strip(): raise ValueError("主题必须保留 evidence") if not isinstance(confidence, (int, float)) or not 0 <= confidence <= 1: raise ValueError("confidence 必须在 0 到 1 之间") used.update(ids)
missing = allowed - used if missing: raise ValueError(f"存在未分组反馈:{sorted(missing)}") return result
def summarize(result: dict) -> list[dict]: """派生统计由程序计算,不让模型直接填写。""" return [ { "theme": theme["name"], "count": len(theme["feedback_ids"]), "confidence": theme["confidence"], "evidence": theme["evidence"], } for theme in result["themes"] ]
if __name__ == "__main__": rows = [ Feedback("F-001", "搜索结果出来得太慢"), Feedback("F-002", "输入关键词后要等待很久"), Feedback("F-003", "导出时希望支持 CSV 格式"), Feedback("F-004", "希望可以关闭部分提醒"), ] result = validate(demo_model(rows), rows) output = {"themes": result["themes"], "summary": summarize(result)} print(json.dumps(output, ensure_ascii=False, indent=2))
|
把代码保存为 feedback_theme_aggregator.py 后运行:
1
| python feedback_theme_aggregator.py
|
程序先验证原始反馈编号,再验证主题结构,最后才计算 summary。如果把 F-002 改成不存在的编号、删除一个主题,或者把置信度改成 1.2,程序都会在输出前抛出错误。这样的失败比把错误结果静默交给产品看板更容易发现。
如何接入真实模型
真实接入时,只替换 demo_model 这一层,让它接收反馈列表并返回 Python 字典;validate 和 summarize 不应绕过。模型请求需要使用服务商当前支持的结构化输出能力,解析失败就作为调用失败处理,而不是用字符串切片“修复”半截 JSON。密钥只能从环境变量读取,不能写进代码、示例配置或日志。
如果模型服务返回的不是严格 JSON,先记录可追踪的请求编号和错误类型,再按重试策略处理;不要把未经解析的文本直接存入主题表。生产环境还应保存模型名称、提示词版本、输入样本版本和校验结果,方便复盘某次聚合为什么发生变化。
结果为什么可复核
可复核不只是“把结果打印出来”。首先,主题只引用输入编号,审核者可以通过编号定位原文;其次,evidence 说明模型依据了哪类描述,但它本身仍然只是摘要,不能替代原文;最后,summary 由程序根据已验证的编号计算,因此主题数量不会因为模型漏算或重复计数而漂移。
实际批处理时,可以把每次输入和输出保存为一条不可变记录,并为记录生成请求编号。审核者修改主题时不要覆盖模型原始结果,而是新增人工结论和审核人信息。这样能够区分“模型当时怎么判断”和“后来人为什么改动”,也能把争议样本加入下一轮离线回归测试。
如果某个主题的置信度低于业务设定的阈值,可以直接进入待审核队列;阈值不是模型参数,而是业务策略,应该通过历史样本评估后确定。对于包含个人信息的反馈,发送模型前还应先做脱敏,日志只保存必要的编号、状态和错误代码。
常见问题
置信度高就能自动排优先级吗? 不能。置信度只表示模型对归类的主观确信程度,不等于用户数量、影响范围或修复价值。优先级应结合业务指标,并保留人工确认。
为什么不让模型直接计算主题数量? 数量是 feedback_ids 的长度,程序计算更准确,也避免模型同时承担生成和统计两个容易互相矛盾的任务。
一条反馈确实属于多个主题怎么办? 把“只能出现一次”的规则改成允许重复,并增加 primary_theme 或权重字段;修改契约后要补充对应测试,不能只放宽校验而不改变审核界面。
演示适配器是不是没有真正使用 AI? 是。它只用于离线验证控制流,没有伪造线上模型结果。真实模型只应替换适配层,输入、校验、证据保留和人工复核边界仍然适用。
小结
这个小项目的重点不是让模型自动决定产品路线,而是把一次开放式归纳限制在可检查的范围内:模型提出主题,程序验证引用关系,统计字段由代码派生,审核者可以沿着 evidence 回到原始反馈。把生成、校验和业务决策分开,才能在更换模型或增加数据量时保持结果可追溯、可回放和可修正。