news 2026/9/7 5:02:50

从零构建中文短文本情绪与意图分析服务:规则词典与FastAPI实践

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
从零构建中文短文本情绪与意图分析服务:规则词典与FastAPI实践

做客服对话分析、用户反馈总结或者社交媒体情绪监控时,业务方最常问的一句话是:用户这句话到底在想什么。为了更清楚地回答这个问题,我通常会先搭建一个轻量级的中文短文本情绪与意图分析服务,项目代号就叫“sakura在想什么呢”。名字来自那句日常口语——看起来是在问一个角色在想什么,实际上是在做一次文本语义结构拆解:判断一句话表达了什么情绪,属于什么意图。

本文会沿着这条主线,从规则词典开始实现一个最小可运行的分析器,再用 FastAPI 把它包装成 HTTP 接口,最后给出调优、排错和生产落地建议。适合刚接触 NLP 工程化、想从零做一个可调用文本分析服务的读者。整个项目不需要 GPU,不需要标注数据,只要 Python 环境和几千行以内的代码就能跑起来。

1. 把“在想什么”翻译成可计算的工程问题

在工程上,回答“在想什么”不是要去揣测用户心理,而是把一段文本映射到有限的结构化标签。以客服场景为例,一句“网络太差了,一直崩溃”和一句“这个功能怎么用啊”,业务方需要知道的信息完全不同。前一句是用户的消极情绪在表达抱怨,后一句是用户的中性情绪在寻求帮助。如果系统能把这两种情况自动分开,后续就可以做工单分级、质量分析和智能回复。

所以这里要拆解两类信号:

“网络太差了,一直崩溃。” -> 情绪:negative;意图:complaint “这个功能怎么用啊?” -> 情绪:neutral;意图:inquiry

一个是情绪倾向,一个是意图类型,二者组合起来才是这句话的完整结构。

1.1 一句话里的两类信号:情绪倾向和意图类型

情绪倾向(Sentiment)描述的是说话人对主题的情感态度。常见粒度有正向、负向、中性,也可以细分到惊讶、愤怒、失望等。情绪分析在文本挖掘里属于句子级分类任务,输入是文本,输出是标签和置信度。

意图识别(Intent Recognition)描述的是这句话想达到什么目的。用户说“怎么登录”,意图是询问;用户说“登录按钮不见了”,意图是问题反馈;用户说“建议增加记住密码”,意图是建议。意图识别通常是一个多分类问题,类别集合需要根据业务场景预先定义。

这两个任务经常一起出现,因为它们在产品上强关联:情绪决定处理优先级,意图决定处理方式。把两者同时输出,比单独做任何一个都更有价值。

1.2 为什么第一版选规则词典而不是深度学习模型

第一版用规则和词典,核心原因有三个:

  1. 不需要标注数据。深度学习模型需要有标签的训练集,冷启动阶段往往没有这些数据。
  2. 可解释、可调试。规则命中哪个关键词、触发哪个情绪词,逻辑是透明的,出问题可以直接改规则。
  3. 速度快、成本低。词典匹配和字符串检索都是毫秒级,不需要加载模型权重,也不依赖 GPU。

等系统运行一段时间,积累了足够多的 bad case 之后,再考虑用模型替换规则,效果会更好。这也符合“先上线、再迭代”的工程节奏。直接在第一版就引入大模型或微调模型,常常会因为数据质量、延迟和成本问题拖慢项目进度。

1.3 项目输入、输出与边界

“sakura在想什么呢”作为最小项目,处理范围这样定义:

  • 输入:单条中文短文本,长度不超过 500 字。
  • 输出:情绪标签、情绪分数、情绪置信度、命中情绪词、意图标签、意图置信度、命中关键词。
  • 不处理多轮对话,不做指代消解,不识别专有名词实体。

这个边界是刻意的。先把单句分析做成稳定接口,后续再扩展上下文和实体抽取。

2. 环境准备:Python依赖、目录结构与规则文件

2.1 Python环境与依赖版本

推荐使用 Python 3.9 及以上版本。依赖方面,核心只需要三个库:jieba 负责中文分词,FastAPI 负责 HTTP 服务,uvicorn 负责启动服务。

依赖版本建议作用
Python3.9+运行环境
jieba0.42.x中文分词
fastapi0.110.xHTTP 服务框架
uvicorn0.29.xASGI 服务器

安装命令:

pip install jieba fastapi uvicorn

如果是为了稳定交付,建议安装后执行pip freeze > requirements.txt固定住实际环境里的版本。下面这份requirements.txt可以作为初始参考,实际项目以自己的解析结果为准:

jieba==0.42.1 fastapi==0.110.0 uvicorn==0.29.0

2.2 项目目录结构

项目结构不需要复杂,但要把“分析逻辑”和“HTTP服务”分开,方便后续测试和扩展:

sakura-thinking/ ├── main.py ├── analyzer.py ├── data/ │ ├── emotion_dict.json │ └── intention_rules.json ├── requirements.txt └── README.md

analyzer.py只负责文本分析,不依赖 FastAPI;main.py负责接收 HTTP 请求,调用分析器并返回结果。规则和词典放在data目录下,因为它们是外部配置,不应该硬编码在代码里。

2.3 用JSON准备情感词典和意图规则

情感词典包含三类信息:正向词、负向词、程度副词、否定词。注意这里的情感词要尽量选择能被 jieba 切分出来的词,例如“差”“崩溃”可以,“太差”这种短语通常会被切分成“太”和“差”,所以建议放在意图规则里用子串匹配。

创建data/emotion_dict.json

{ "positive": ["开心", "喜欢", "满意", "期待", "好用", "感谢", "点赞"], "negative": ["差", "糟糕", "生气", "难过", "失望", "垃圾", "崩溃", "讨厌", "难用", "故障"], "degree": { "非常": 1.5, "特别": 1.5, "太": 1.3, "很": 1.2, "有点": 0.8, "稍微": 0.6 }, "negation": ["不", "没", "没有", "别", "不太", "无法"] }

创建data/intention_rules.json

{ "praise": { "priority": 10, "keywords": ["满意", "好用", "喜欢", "点赞", "太棒了"] }, "complaint": { "priority": 10, "keywords": ["太差", "垃圾", "慢死了", "故障", "崩溃", "难用", "不行"] }, "thanks": { "priority": 8, "keywords": ["感谢", "谢谢", "辛苦了"] }, "suggestion": { "priority": 6, "keywords": ["建议", "希望", "能不能", "可不可以", "如果"] }, "inquiry": { "priority": 5, "keywords": ["怎么", "为什么", "如何", "可以吗", "哪里", "是否"] } }

为什么要用 JSON 而不是直接写在 Python 代码里?因为规则会频繁变化。业务方觉得“易仕”这个词属于投诉时,只需要改 JSON 让运营人员维护即可,不需要改动代码。priority字段用来处理多意图冲突,例如“希望你说一下这个功能怎么用”,同时命中了suggestioninquiry,此时按优先级取高者会更合理。

3. 核心分析器实现:清洗、分词、情绪打分与意图匹配

3.1 文本清洗和分词:把一句话变成可匹配的最小单元

文本清洗要解决的问题是“如何不被标点和符号干扰”。中文文本里常见的逗号、句号、感叹号、问号,以及英文标点,都需要在匹配前去掉,否则“怎么用?”里的问号会影响规则判断。

分词使用 jieba。例如:

import jieba print(list(jieba.cut("网络太差了,一直崩溃")))

输出类似:['网络', '太', '差', '了', ',', '一直', '崩溃']。分完词之后,情绪分析就可以对每个词做词典匹配,意图分析则可以直接用“原文子串匹配”,因为意图关键词往往是短语,例如“慢死了”“太差了”,子串匹配更稳定。

3.2 情绪打分:情感词、程度副词和否定词怎么配合

情绪打分的核心规则是这样:

  • 遇到正向词,加 1 分。
  • 遇到负向词,减 1 分。
  • 如果情感词前一个窗口内出现程度副词,则把分数乘以对应权重。
  • 如果出现否定词,则把分数符号反转,也就是“不开心”应该识别为负面。
  • 最终把总分限制在[-3, 3]之间,避免极端值影响展示。

创建analyzer.py

import json import re from typing import List import jieba class SakuraAnalyzer: def __init__(self, emotion_dict_path: str, intention_rules_path: str): self.emotion_dict = self._load_json(emotion_dict_path) self.intention_rules = self._load_json(intention_rules_path) self.positive_words: set = set(self.emotion_dict.get("positive", [])) self.negative_words: set = set(self.emotion_dict.get("negative", [])) self.degree_words: dict = self.emotion_dict.get("degree", {}) self.negation_words: set = set(self.emotion_dict.get("negation", [])) self.punctuation_regex = re.compile(r"[\s。,!?!?,.;;、~~\-—]") # 预热分词器,避免首次请求耗时过高 jieba.lcut("初始化分词器") @staticmethod def _load_json(path: str): with open(path, "r", encoding="utf-8") as f: return json.load(f) def clean_text(self, text: str) -> str: text = self.punctuation_regex.sub("", text) return text.strip() def segment(self, text: str) -> List[str]: return list(jieba.cut(text)) def _local_weight(self, words: List[str], index: int, window: int = 2): weight = 1.0 negated = False start = max(0, index - window) for j in range(index - 1, start - 1, -1): w = words[j] if w in self.degree_words: weight *= self.degree_words[w] elif w in self.negation_words: negated = not negated return weight, negated def analyze_emotion(self, text: str): cleaned = self.clean_text(text) words = self.segment(cleaned) score = 0.0 hit_words = [] for i, word in enumerate(words): if word in self.positive_words: weight, negated = self._local_weight(words, i) delta = weight if not negated else -weight score += delta hit_words.append(word) elif word in self.negative_words: weight, negated = self._local_weight(words, i) delta = -weight if not negated else weight score += delta hit_words.append(word) score = max(-3.0, min(3.0, score)) if score > 0.2: emotion = "positive" elif score < -0.2: emotion = "negative" else: emotion = "neutral" confidence = min(0.95, 0.5 + abs(score) * 0.15) return { "emotion": emotion, "emotion_score": round(score, 4), "emotion_confidence": round(confidence, 4), "emotion_hit_words": hit_words, } def analyze_intention(self, text: str): cleaned = self.clean_text(text) results = [] for intent_name, config in self.intention_rules.items(): hit = [kw for kw in config["keywords"] if kw in cleaned] if hit: results.append({ "intention": intent_name, "hit_count": len(hit), "priority": config.get("priority", 0), "keywords": hit, }) if not results: return { "intention": "other", "confidence": 0.1, "matched_keywords": [], } results.sort(key=lambda x: (-x["priority"], -x["hit_count"])) top = results[0] confidence = min(0.9, 0.5 + 0.1 * top["hit_count"]) return { "intention": top["intention"], "confidence": round(confidence, 4), "matched_keywords": top["keywords"], } def analyze(self, text: str): emotion_result = self.analyze_emotion(text) intention_result = self.analyze_intention(text) return { "input_text": text, **emotion_result, **intention_result, }

这里有两个关键方法要重点解释。

_local_weight方法负责处理否定词和程度副词。它从当前情感词的位置往前最多看两个词,倒序遍历,遇到程度副词就累乘权重,遇到否定词就翻转一次符号。这种窗口机制适合短句,能覆盖大多数“不太好”“非常差”这类结构,但不适合处理很长的多重否定句。

analyze_emotion里的置信度公式是简单启发式:情绪越极端,置信度越高。0.5 + abs(score) * 0.15表示基础置信度 0.5,每偏离中性 1 分增加 0.15,上限 0.95。

3.3 意图识别:关键词命中、优先级和置信度

意图识别的实现比情绪识别更简单,本质是对原文做关键词子串匹配。因为“太差”这种短语在分词后会被拆开,而子串匹配可以完整命中。

多个意图同时命中时,按priority降序、命中次数降序排序。例如“这个功能太差了,建议优化”同时命中complaint的“太差”和suggestion的“建议”,因为complaint的 priority 是 10,suggestion是 6,最终输出complaint

置信度公式是0.5 + 0.1 * hit_count,上限 0.9。这样设计是为了让“单一关键词命中”不至于给出过高的置信度,至少留出优化空间。

3.4 把两类结果组装成统一返回结构

返回结构里同时包含原始文本、情绪信息、意图信息。这样一个前端或下游服务只需要调用一次接口,就能拿到完整分析结果。字段设计如下:

字段类型含义
input_textstring原始输入文本
emotionstringpositive / negative / neutral
emotion_scorefloat情绪总分,范围约 -3 到 3
emotion_confidencefloat情绪置信度
emotion_hit_wordsarray命中的情绪词
intentionstring意图标签
confidencefloat意图置信度
matched_keywordsarray命中的意图关键词

这种结构的好处是:调试时可以直接看emotion_hit_wordsmatched_keywords,知道结果是由哪些词触发的。

4. 用FastAPI暴露分析接口:单条与批量的服务结构

4.1 搭建FastAPI应用和请求校验模型

main.py里做三件事:加载分析器、定义请求和响应模型、暴露接口。为了让服务不依赖当前工作目录,这里用Path(__file__).resolve().parent构造词典路径。

import logging import time from pathlib import Path from typing import List from fastapi import FastAPI, HTTPException from pydantic import BaseModel, Field from analyzer import SakuraAnalyzer logging.basicConfig( level=logging.INFO, format="%(asctime)s %(levelname)s %(name)s %(message)s", ) logger = logging.getLogger("sakura") BASE_DIR = Path(__file__).resolve().parent analyzer = SakuraAnalyzer( str(BASE_DIR / "data" / "emotion_dict.json"), str(BASE_DIR / "data" / "intention_rules.json"), ) app = FastAPI(title="sakura在想什么呢", version="0.1.0") class AnalyzeRequest(BaseModel): text: str = Field(..., min_length=1, max_length=500, description="待分析文本") class AnalyzeResponse(BaseModel): input_text: str emotion: str emotion_score: float emotion_confidence: float emotion_hit_words: List[str] intention: str confidence: float matched_keywords: List[str] class BatchAnalyzeRequest(BaseModel): texts: List[str]

初始化分析器放在模块顶层,是为了让服务启动时只加载一次规则和分词器,而不是每个请求都重复加载。jieba.lcut("初始化分词器")这行预热代码能明显减少第一次请求的耗时。

4.2 单条分析接口

单条接口接收 JSON 请求体,返回AnalyzeResponse。为了观察线上效果,在接口里记录耗时和输入文本。

@app.post("/analyze", response_model=AnalyzeResponse) def analyze_single(req: AnalyzeRequest): start = time.time() result = analyzer.analyze(req.text) cost_ms = (time.time() - start) * 1000 logger.info("analyze text=%s cost=%.2fms", req.text, cost_ms) return result

这里不手动做异常捕获,因为通用异常会交给 FastAPI 返回 500,参数错误会返回 422。生产环境再加入业务告警即可。

4.3 批量分析接口和请求日志

批量接口用于分析多条文本,比如一次性处理一批问卷反馈。批量条数限制在 100 条以内,避免单次请求占用过多 CPU。

@app.post("/analyze/batch") def analyze_batch(req: BatchAnalyzeRequest): if not req.texts: raise HTTPException(status_code=400, detail="texts 不能为空") if len(req.texts) > 100: raise HTTPException(status_code=400, detail="单次最多分析 100 条") results = [] for text in req.texts: if not text.strip(): raise HTTPException(status_code=400, detail="批量文本中不能包含空字符串") results.append(analyzer.analyze(text)) logger.info("batch analyze count=%d cost=%.2fms", len(req.texts), time.time() * 1000) return {"total": len(results), "results": results}

批量接口的返回值不直接绑定AnalyzeResponse,因为里面需要包一层total字段。这样调用方可以根据total判断是否丢数据。

5. 运行验证:启动服务、curl调用与bad case复盘

5.1 安装依赖并启动本地服务

在项目根目录执行:

cd sakura-thinking pip install -r requirements.txt uvicorn main:app --reload --port 8000

正常情况下可以看到类似输出:

INFO: Uvicorn running on http://127.0.0.1:8000

--reload参数适合开发环境,改代码后服务会自动重启。生产环境不要加--reload

5.2 用curl验证三类典型文本

打开一个新终端,执行下面的请求。

第一类,中性询问:

curl -s -X POST http://127.0.0.1:8000/analyze \ -H "Content-Type: application/json" \ -d '{"text": "这个功能怎么用啊?"}'

预期响应:

{ "input_text": "这个功能怎么用啊?", "emotion": "neutral", "emotion_score": 0.0, "emotion_confidence": 0.5, "emotion_hit_words": [], "intention": "inquiry", "confidence": 0.6, "matched_keywords": ["怎么"] }

第二类,负面抱怨:

curl -s -X POST http://127.0.0.1:8000/analyze \ -H "Content-Type: application/json" \ -d '{"text": "网络太差了,一直崩溃"}'

预期响应:

{ "input_text": "网络太差了,一直崩溃", "emotion": "negative", "emotion_score": -2.3, "emotion_confidence": 0.845, "emotion_hit_words": ["差", "崩溃"], "intention": "complaint", "confidence": 0.6, "matched_keywords": ["崩溃"] }

注意这里情绪命中词是“差”和“崩溃”,而不是“太差”,因为 jieba 把“太差了”切成了“太”“差”“了”。意图识别用子串匹配,所以能命中“崩溃”。“太差”如果出现在文本中,也会被意图规则捕获。

第三类,正向表扬:

curl -s -X POST http://127.0.0.1:8000/analyze \ -H "Content-Type: application/json" \ -d '{"text": "你们这个工具很好用,我非常满意"}'

预期响应:

{ "input_text": "你们这个工具很好用,我非常满意", "emotion": "positive", "emotion_score": 2.7, "emotion_confidence": 0.905, "emotion_hit_words": ["好用", "满意"], "intention": "praise", "confidence": 0.7, "matched_keywords": ["好用", "满意"] }

“好用”前面有“很”,程度权重 1.2;“满意”前面有“非常”,程度权重 1.5,所以总分为 2.7。这说明程度副词机制生效了。

5.3 从bad case反推规则缺陷

规则系统一定会遇到 bad case,关键是能快速定位原因。举个例子,如果输入“这个方案不是不行”:

  • jieba 切分可能得到“这个/方案/不是/不行”。
  • 意图规则中“不行”属于 complaint,所以会输出“抱怨”。
  • 但这句话的真实语义是“这个方案是可以的”。

出现这种情况有两个原因。第一,意图规则没有处理否定词;第二,业务中没有为“否定+负面词”的双重否定场景定义规则。解决方法是把这类句式加入正则例外,或者在业务对接层面接受这个误判率,等数据量大了再交给模型处理。

排查 bad case 时,不要只盯着最终标签,要看中间结果:emotion_hit_words命中了什么,matched_keywords命中了什么。这两个字段就是整个系统的可解释性。

6. 调优:词典粒度、否定词窗口、规则优先级与置信度

6.1 情感词典的粒度要与分词结果对齐

“太差”在情感词典里是负面词,但 jieba 分词成“太”和“差”,所以“太差”作为词典项几乎不会被命中。这解释了为什么情感词典里要放“差”,而不是只放“太差”。

一个实用的调优流程是:

  1. 收集一批代表业务场景的真实句子。
  2. 执行list(jieba.cut(sentence)),把分词结果打印出来。
  3. 观察哪些词反复出现且明显有情绪倾向,把它们加入词典。
  4. 观察多字短语和分词的差异,用短语匹配补充。

这样比凭感觉堆词典词更高效。

6.2 否定词和程度副词的上下文窗口

_local_weight的窗口默认是 2,也就是只看情感词前面的两个词。这个值不是越大越好。

窗口太大会导致远距离否定词误伤,例如“我今天早上没有吃饭,但是很开心”中的“没有”离“开心”太远,不应该影响情绪。窗口太小的缺点是无法处理“不是很满意”这种连续结构,因为“很”和“不”都在窗口内,窗口至少要为 2。

建议先保留window=2,然后结合自己的语料统计。如果发现三重否定或长距离否定经常出现,就需要换思路:先做否定范围识别,再计算情绪分。

6.3 意图关键词需要避免互相覆盖

当前规则中suggestion包含“如果”,inquiry包含“怎么”。句子“如果不知道怎么操作,你会怎么处理”会同时命中多个意图。虽然priority可以排序,但如果业务上希望这类句子归为“询问”,就要调整关键词或优先级。

以下是几个常见冲突和调整建议:

冲突场景当前配置可能误判调整方向
“能不能”同时是询问还是建议suggestion 命中可能忽略 inquiry优先保留 suggestion,或根据上下文切换
“怎么”在提问中也可能出现在抱怨inquiry 优先级低抱怨被判为询问观察数据分布后调 priority
“不行”可能是否定反馈complaint 命中双重否定误判引入否定词前置例外规则
“建议”在商务文本中可能是名词suggestion 命中误判维护领域停用词或白名单

调优的核心不是设计一套万能规则,而是建立 bad case 集,每次改规则都跑一遍回归,确保不会修一个新问题拆掉一个旧功能。

6.4 阈值与置信度的调节方式

当前情绪判定阈值是 0.2,也就是说分数大于 0.2 判为正,小于 -0.2 判为负,之间判为中性。这个值可以调:

  • 调大阈值,会有更多句子被判为中性,减少误判,但会漏掉一些弱情绪表达。
  • 调小阈值,能捕捉更多弱情绪,但中性噪声会变多。

置信度公式同样可以调整。如果业务对“高置信度”有更严格的要求,可以把公式改成0.5 + abs(score) * 0.2,上限保持不变;更合理的方式是准备一小批标注样本,统计分数和真实标签的关系,拟合一个更合适的映射函数。

7. 常见问题与排查:从分词不一致到请求报错

7.1 先按这个顺序排查,能省一半时间

遇到问题不要直接怀疑代码,按顺序检查:

  1. 输入是否正确:请求体字段名、Content-Type、中文字符编码。
  2. 文件路径是否正确:启动命令所在目录、JSON 文件是否存在。
  3. 词典和规则文件是否加载:打印len(analyzer.positive_words)等字段。
  4. 分词结果是否符合预期:把jieba.lcut的结果打印出来。
  5. 匹配逻辑是否正确:确认是子串匹配还是分词匹配。
  6. 服务是否加载了最新规则:--reload是否生效,必要时手动重启。
  7. 端口和进程是否正常:8000 是否被占用。
  8. 日志里有哪条明确异常。

这个顺序的核心逻辑是:先排除输入问题和环境问题,再排查逻辑问题。

7.2 高频问题速查表

问题现象可能原因检查方式处理建议
所有文本都返回中性情绪情感词典没加载成功,或情感词与分词不一致打印positive_wordsnegative_words长度,打印分词结果检查 JSON 路径和 UTF-8 编码,扩充与分词对齐的词
“太差了”识别为抱怨但情绪为 neutral“太差”没有被分词命中,可能只命中了“差”但词典缺少“差”打印emotion_hit_words在负向词典中加入“差”,确认程度副词“太”生效
多字关键词始终不命中词典里的词是 jieba 切不开的长词打印jieba.lcut结果改用子串匹配,或把短语放入意图规则
批量接口返回 400texts 为空或包含空字符串检查请求体增加字段校验,前端过滤空行
接口返回 422字段名写错或 JSON 格式不对查看 FastAPI 返回的错误详情对照AnalyzeRequest字段名修正
端口被占用上一次服务未退出lsof -i:8000或 `netstat -anofindstr 8000`
首次请求特别慢jieba 首次加载词典耗时较高看日志时间戳初始化时执行一次“预热”切分

7.3 规则类问题如何定位:打印中间结果

如果结果不符合预期,最直接的方法是在接口外写一段临时脚本,直接调用SakuraAnalyzer的中间方法:

from analyzer import SakuraAnalyzer analyzer = SakuraAnalyzer( "data/emotion_dict.json", "data/intention_rules.json", ) text = "这个方案不是不行" print("clean:", analyzer.clean_text(text)) print("words:", analyzer.segment(text)) print("emotion:", analyzer.analyze_emotion(text)) print("intention:", analyzer.analyze_intention(text))

这样能一次性看到清洗后文本、分词结果、情绪命中词和意图命中词,规则缺陷会变得非常直观。相比直接调用 HTTP 接口,这种方式更适合调试。

8. 从学习项目到生产:模型化、监控与交付清单

8.1 学习环境能跑通,生产环境还需要补什么

学习环境只要服务能启动、接口能返回结果就行。生产环境还需要补齐下面这些能力:

  1. 配置外置化。词典路径、规则路径、服务端口、日志级别不能硬编码,要用环境变量或配置文件控制。
  2. 日志结构化。不要只打印一行文本,建议输出 JSON 日志,包含text_hashcost_msemotionintention等字段。
  3. 结果监控。统计每类情绪和意图的数量占比、平均耗时、batch 大小,用指标暴露给监控系统。
  4. 规则版本管理。JSON 规则文件纳入 Git,版本号要随着规则更新而递增,方便回滚。
  5. 高频词缓存。如果同一个文本会被重复分析,可以在 Redis 或本地内存里做一层缓存,减少重复计算。
  6. 异常兜底。不要让分析器抛异常直接击穿接口,可以记录日志后返回一个默认的other结果。

8.2 扩展方向:槽位抽取、多轮上下文与模型化

“sakura在想什么呢”目前只能输出情绪和意图。实际产品里往往还需要更多信息。

槽位抽取是第一个扩展方向。例如用户问“导出功能在哪里”,意图是询问,但“导出功能”是用户要问的对象。可以用正则或序列标注模型把关键实体抽取出来,直接跳到对应文档或操作入口。

多轮上下文是第二个方向。用户在前一轮问了“导出功能怎么用”,下一轮说“那删除功能呢”,这里的“那删除功能呢”只有结合上文才能判断意图。这时候需要为每个会话维护一个上下文状态。

模型化是第三个方向。词典规则在冷启动阶段好使,但泛化能力有限。当积累了几千条带标签数据后,可以用 BERT 类模型做文本分类,输出语情绪和意图。同时保留规则结果作为兜底,当模型置信度低时回退到规则。

无论是规则还是模型,输出结构都不要变。这样下游系统不用关心上游是怎么算出来的,只需要消费统一的 JSON 结构。

8.3 可复用的交付检查清单

把这个项目从本地脚本升级成可交付服务前,可以按下面这份清单做检查:

  • [ ]requirements.txt已固定实际安装版本
  • [ ] 数据目录放在项目内部,使用基于Path(__file__)的绝对路径
  • [ ] 情感词典是 UTF-8 编码,能覆盖 positive / negative / neutral 三类验证样本
  • [ ] 意图规则覆盖 praise / complaint / thanks / suggestion / inquiry 五类验证样本
  • [ ] 单条接口和批量接口都做过 curl 验证
  • [ ] 至少准备了 10 条 bad case 作为回归测试集
  • [ ] 请求日志记录了输入文本、耗时和结果,且不包含用户敏感信息
  • [ ] 服务启动无 warning,首次请求耗时在可接受范围
  • [ ] 配置文件没有硬编码绝对路径和密钥
  • [ ] README 写清楚了启动命令和数据目录结构

如果后续团队已经有标注数据和 GPU 资源,可以把规则结果作为 baseline,再用 ERNIE 或 BERT 模型做意图分类。这样既保留了可解释的兜底逻辑,又能用模型提高泛化能力。当前这个项目的最小闭环,就是先把情绪和意图两类信号输出稳定,至于“sakura在想什么”这个问题能回答到什么程度,取决于词典覆盖率、规则质量和后续模型迭代。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/7 5:01:17

高通平台新增QMI接口实战:从内核配置到用户态验证全流程

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/7 5:01:11

C#上位机集成虹软ArcFace SDK实现离线人脸识别实战

简介&#xff1a;一套基于虹软免费SDK的C#人脸识别Demo完整版&#xff0c;面向需要在.NET环境中快速实现人脸检测、人脸对比与人脸检索的开发者&#xff0c;适合作为WinForms桌面项目的参考工程。压缩包内共包含二十五份文件&#xff0c;核心是十七份C#源代码文件&#xff0c;完…

作者头像 李华
网站建设 2026/9/7 5:00:12

Verilog计数器必知:阻塞与非阻塞赋值底层原理解析与实战

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/7 4:59:01

PTN业务配置核心流程与故障排查实战指南

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/7 4:56:37

局域网NTP时间同步服务器搭建:chrony配置与客户端接入指南

简介&#xff1a;这是一套面向小型局域网的时间同步工具&#xff0c;包含服务器端与客户端程序&#xff0c;适合需要在无外部NTP条件下自行校准设备时钟的IT运维或开发人员。方案以Visual C编写客户端&#xff0c;通过自定义协议与指定服务器通信&#xff0c;完成时间请求、响应…

作者头像 李华