15-文本分析与情感分析

文本分析是生成式 AI 在企业场景里最容易落地、也最容易证明价值的一类能力。因为很多业务问题本质上都和"读懂文本"有关,例如分析用户评论、识别投诉、提取关键词、总结工单内容、给留言自动分类等。过去这些任务往往依赖规则、关键词匹配或传统机器学习,而现在大语言模型让它们的门槛显著降低。

本篇会围绕 .NET 开发中最常见的文本理解任务展开,重点讲清楚情感分析、关键词提取、摘要生成、实体识别和意图判断的实现思路。文章仍然坚持"先解释概念,再看代码,再拆解代码,再讨论适用场景和注意点"的节奏,适合正在做评论分析、客服质检、舆情监控或内部文本处理工具的读者。

一、先理解文本分析在业务里解决什么问题

1.1 文本分析不是只能做分词统计

很多开发者一听到文本分析,第一反应还是关键词搜索、词频统计或规则匹配。这些方法并没有过时,尤其在结构稳定、词汇固定的场景里依然很有价值。但它们通常很难处理语气、上下文和隐含意图。例如"也就那样吧""速度倒是快,就是总崩"这类表达,单看关键词往往并不能准确判断情绪和态度。

大语言模型在这里带来的变化,是它能在一个统一接口里完成更偏理解型的任务。你不必为每个小需求单独训练模型,也不必先做复杂特征工程,而是可以通过 Prompt 直接让模型输出情感、关键词、摘要或结构化 JSON。这非常适合业务迭代快、规则变化多、数据格式不够规范的系统。

1.2 情感分析适合哪些典型场景

情感分析可以理解成判断一段文本在表达什么态度。最基础的输出是正面、负面和中性,但在真实项目里,它往往还会伴随情感强度、原因说明和意图识别。例如一条评论被判定为负面之后,业务通常还想知道它是在抱怨价格、吐槽性能,还是不满售后服务。

这类能力常见于商品评论分析、应用商店评价监控、客服对话质检、舆情监测和员工反馈归纳。它的价值不只是"看起来智能",而是能把大量原本需要人工逐条阅读的文本,先自动分出优先级和类别,让后续人工处理更聚焦。换句话说,情感分析最适合做第一轮筛选和归纳,而不是替代一切最终判断。

二、先从最常见的情感判断做起

2.1 单条文本情感分类的基本思路

最简单的情感分析目标,是让模型判断一条文本偏正面、偏负面还是中性。虽然这看起来像一个分类问题,但在生成式 AI 时代,我们完全可以通过 Prompt 直接让模型输出结构化结果,而不必先训练专门分类器。对业务原型和小规模应用来说,这种方式通常更灵活。

csharp 复制代码
using System.Text.Json;
using Microsoft.SemanticKernel;

public sealed record SentimentResult(string Sentiment, double Score, string Reason);

var apiKey = Environment.GetEnvironmentVariable("OPENAI_API_KEY")
    ?? throw new InvalidOperationException("请先配置 OPENAI_API_KEY。");

var kernel = Kernel.CreateBuilder()
    .AddOpenAIChatCompletion(modelId: "gpt-4o-mini", apiKey: apiKey)
    .Build();

var prompt = """
请分析下面文本的情绪,并只返回 JSON:
{
  "sentiment": "Positive | Neutral | Negative",
  "score": 0.0,
  "reason": "不超过 40 个字"
}

文本:{{$input}}
""";

var response = await kernel.InvokePromptAsync(
    prompt,
    new KernelArguments { ["input"] = "新版本界面更清爽,加载速度也明显变快了。" });

var result = JsonSerializer.Deserialize<SentimentResult>(
    response.ToString(),
    new JsonSerializerOptions { PropertyNameCaseInsensitive = true });

Console.WriteLine($"情感:{result?.Sentiment}");
Console.WriteLine($"评分:{result?.Score}");
Console.WriteLine($"原因:{result?.Reason}");

这个示例使用了前面学过的 Semantic Kernel,但思路同样适用于直接调用模型 API。prompt 里最关键的点,是明确要求"只返回 JSON",并把字段结构提前约束好。这样做的目的不是让 Prompt 更好看,而是为了后续能够稳定反序列化成 SentimentResult 对象。Score 字段通常用来表示情绪强弱,Reason 则方便你后续做人工复核或日志分析。

需要特别提醒的是,模型返回 JSON 并不等于永远不会出错。真实项目里仍然要考虑格式异常、字段缺失和重试策略。不过从教学角度看,这段代码已经完整体现了"Prompt 约束输出 → 反序列化为对象 → 在 .NET 中继续处理"的基本流程。

2.2 为什么很多业务还需要情感评分与解释

如果系统只返回正面、负面和中性三个标签,很多时候还不够用。因为业务真正关心的往往是:负面到底有多强烈,是否值得优先人工介入,模型为什么这么判断。比如"有点慢"和"慢得根本不能用"都可能是负面,但处理优先级显然不同。

csharp 复制代码
var prompt = """
请分析下面评论的情感强度,并只返回 JSON:
{
  "sentiment": "Positive | Neutral | Negative",
  "score": 0.0,
  "confidence": 0.0,
  "reason": "简要说明判断依据"
}

说明:score 越接近 1 表示情绪越强烈;confidence 表示判断把握。
评论:{{$comment}}
""";

var response = await kernel.InvokePromptAsync(
    prompt,
    new KernelArguments { ["comment"] = "功能勉强能用,但页面卡顿严重,提交时还经常失败。" });

Console.WriteLine(response);

在这个示例里,scoreconfidence 分别承担了两个不同职责。前者更像业务强度评分,用于排序、预警和优先级判断;后者则表示模型对当前结论的把握程度。它们并不是绝对客观的统计值,而是模型给出的结构化估计,所以更适合作为辅助判断依据,而不是唯一决策依据。

三、把其他常见文本任务一起串起来

3.1 关键词提取和摘要生成为什么常常一起出现

在很多文本处理场景里,业务并不是只想知道情感,还希望快速抓住内容主旨。关键词提取可以帮助建立标签、搜索索引和主题聚类,摘要生成则适合让运营、客服或管理者在大量文本中快速理解核心信息。这两个任务经常一起出现,因为它们都在回答同一个问题:这段文本主要在说什么。

csharp 复制代码
var prompt = """
请完成两个任务,并只返回 JSON:
{
  "keywords": ["关键词1", "关键词2", "关键词3", "关键词4"],
  "summary": "一句话摘要"
}

文本:{{$text}}
""";

var response = await kernel.InvokePromptAsync(
    prompt,
    new KernelArguments
    {
        ["text"] = "用户认为新版后台的筛选功能更清晰,但导出报表速度仍然偏慢,希望后续版本进一步优化性能。"
    });

Console.WriteLine(response);

这个 Prompt 的设计体现了一个常见技巧:把多个强相关任务放在一次调用中完成。只要输入文本相同、输出结构清晰,合并调用通常可以节省一次请求开销,并让结果之间保持语义一致。比如同一条评论里提取出的关键词和生成的摘要,如果来自同一次分析,通常更容易互相对照和验证。

3.2 实体识别和意图识别如何帮助业务分流

除了"这段话是好评还是差评",很多业务系统还想知道文本里提到了谁、哪里、什么时间,以及用户究竟想干什么。这就是实体识别和意图识别的价值。前者帮助你抽取结构化信息,后者帮助你决定后续流程,例如是转人工、进入投诉工单,还是触发查询逻辑。

csharp 复制代码
var prompt = """
请分析下面文本,并只返回 JSON:
{
  "intent": "咨询 | 投诉 | 建议 | 表扬 | 其他",
  "persons": [],
  "organizations": [],
  "locations": [],
  "dates": []
}

文本:{{$text}}
""";

var response = await kernel.InvokePromptAsync(
    prompt,
    new KernelArguments
    {
        ["text"] = "张三昨天在北京门店购买了会员服务,但今天联系微软合作团队时仍然无法使用,希望尽快处理。"
    });

Console.WriteLine(response);

这个结果如果被正确解析,你就能同时获得两个层面的信息:一是这条文本更像投诉还是咨询,二是它涉及哪些实体。对于客服系统来说,这些字段可以直接用于路由、工单标签、统计报表和搜索索引。也正因为它们经常成为后续自动化流程的输入,所以在设计 Prompt 时一定要尽量保持字段稳定,别让输出结构频繁变化。

四、实战:把评论分析封装成服务

4.1 用服务类统一管理 Prompt、解析和缓存

当文本分析能力开始被多个接口或多个场景复用时,把逻辑收进服务类是非常自然的一步。这样做的目的不仅是代码更整洁,更重要的是你可以把 Prompt、JSON 解析、缓存和错误处理集中起来,避免每个接口各自复制一套分析逻辑。

csharp 复制代码
using System.Collections.Concurrent;
using System.Text.Json;
using Microsoft.SemanticKernel;

public sealed class CommentAnalysis
{
    public string Sentiment { get; set; } = string.Empty;
    public double Score { get; set; }
    public string Intent { get; set; } = string.Empty;
    public string Summary { get; set; } = string.Empty;
    public List<string> Keywords { get; set; } = new();
}

public sealed class CommentAnalysisService
{
    private readonly Kernel _kernel;
    private readonly ConcurrentDictionary<string, CommentAnalysis> _cache = new();

    public CommentAnalysisService(string apiKey)
    {
        _kernel = Kernel.CreateBuilder()
            .AddOpenAIChatCompletion(modelId: "gpt-4o-mini", apiKey: apiKey)
            .Build();
    }

    public async Task<CommentAnalysis> AnalyzeAsync(string comment)
    {
        if (_cache.TryGetValue(comment, out var cached))
        {
            return cached;
        }

        var prompt = """
        请分析下面评论,并只返回 JSON:
        {
          "sentiment": "Positive | Neutral | Negative",
          "score": 0.0,
          "intent": "咨询 | 投诉 | 建议 | 表扬 | 其他",
          "summary": "一句话总结",
          "keywords": ["关键词1", "关键词2", "关键词3"]
        }

        评论:{{$comment}}
        """;

        var response = await _kernel.InvokePromptAsync(
            prompt,
            new KernelArguments { ["comment"] = comment });

        var result = JsonSerializer.Deserialize<CommentAnalysis>(
            response.ToString(),
            new JsonSerializerOptions { PropertyNameCaseInsensitive = true })
            ?? throw new InvalidOperationException("模型返回内容无法解析为 CommentAnalysis。");

        _cache[comment] = result;
        return result;
    }
}

这个服务类把几件原本分散的事情统一了起来。CommentAnalysis 是输出模型,用来承接结构化结果;CommentAnalysisService 负责持有 Kernel 并执行实际分析;ConcurrentDictionary 则提供一个最简单的内存缓存,避免相同评论被重复分析。这里的缓存思路很适合评论分析、FAQ 归类和重复工单处理这类场景,因为相同内容在真实业务中经常会反复出现。

4.2 再把能力通过 Web API 暴露出来

一旦服务层稳定,提供 API 就是很自然的下一步。评论分析类功能通常会被运营后台、客服系统、管理报表或批处理任务调用,所以把它做成独立接口非常常见。对于批量分析场景,还可以在接口层统一调度并发策略和失败兜底。

csharp 复制代码
var builder = WebApplication.CreateBuilder(args);

var apiKey = Environment.GetEnvironmentVariable("OPENAI_API_KEY")
    ?? throw new InvalidOperationException("请先配置 OPENAI_API_KEY。");

builder.Services.AddSingleton(new CommentAnalysisService(apiKey));

var app = builder.Build();

app.MapPost("/api/comments/analyze", async (AnalyzeRequest request, CommentAnalysisService service) =>
{
    var result = await service.AnalyzeAsync(request.Comment);
    return Results.Ok(result);
});

app.MapPost("/api/comments/analyze-batch", async (BatchAnalyzeRequest request, CommentAnalysisService service) =>
{
    var tasks = request.Comments.Select(service.AnalyzeAsync);
    var results = await Task.WhenAll(tasks);
    return Results.Ok(results);
});

app.Run();

public sealed record AnalyzeRequest(string Comment);
public sealed record BatchAnalyzeRequest(List<string> Comments);

这个接口层设计故意保持简单,好让你把注意力放在结构上。单条分析接口适合实时场景,批量分析接口适合后台任务或运营导入。Task.WhenAll 能够提高吞吐,但在生产环境里你通常还要结合并发限制和重试机制,否则短时间内大量请求可能触发上游限流。也就是说,批量处理的关键不只是并行更快,还包括并行要有边界。

五、可靠性、成本与注意事项

5.1 为什么格式稳定和成本控制同样重要

当文本分析只是一天几十次调用时,很多问题并不明显;但一旦进入批量评论、客服会话和舆情监控这类高频场景,成本和稳定性就会迅速变成核心问题。最典型的风险有两个:一是模型返回的 JSON 偶尔不完全符合预期,导致反序列化失败;二是大量重复文本被重复分析,造成不必要的费用和延迟。

因此,在工程上至少要形成几个习惯。第一,Prompt 中尽量固定输出结构,并在服务层对解析失败做好兜底。第二,给高频文本分析加缓存,尤其是重复评论和模板化留言。第三,批量分析时控制并发,不要为了追求速度把上游服务打爆。第四,对真正需要高精度的场景保留人工复核入口,不要把模型输出直接当成唯一真相。

5.2 为什么不能忽视误判和敏感数据问题

文本分析看起来只是读懂一句话,实际上很容易受到语气、上下文、行业术语和讽刺表达的影响。模型可能把抱怨式建议误判成纯负面,也可能把礼貌包装下的投诉识别成普通咨询。因此,在设计业务流程时,应该接受一个现实:模型输出适合作为辅助判断和优先级工具,而不是完全不需要监督的终审系统。

另外,很多文本分析任务涉及用户留言、客服对话和工单内容,这些数据往往包含邮箱、手机号、地址、订单号等敏感信息。正式项目里,至少要考虑脱敏、最小必要上传和访问审计。只有当你同时把效果、成本、可靠性和安全性都纳入设计,文本分析能力才算真正具备上线价值。

练习题:

  1. 请把本文的情感分类示例扩展成"同时返回情感、评分和处理优先级"的 JSON 结果,并说明各字段的业务意义。
  2. 假设你要分析 1 万条商品评论,为什么缓存、批量并发控制和解析失败兜底都不能省略?
  3. 对于"这个功能也就那样吧"这类语气含糊的评论,你会如何设计人工复核和模型阈值策略来降低误判风险?
相关推荐
铁打的阿秀5 小时前
.net C# 打印pdf添加水印实现
pdf·c#·.net
weixin_397578026 小时前
python docker 微服务怎么通过pycharm 逐行调试
微服务
掘根6 小时前
【微服务即时通讯】语言识别子服务
微服务·云原生·架构
RemainderTime6 小时前
(十一)Spring Cloud Alibaba 2023.x:构建分布式全链路日志追踪体系
分布式·微服务·架构·gateway
我是唐青枫6 小时前
C#.NET stackalloc 深入解析:栈上分配、Span 配合与使用边界
c#·.net
摇滚侠6 小时前
JAVA 项目教程《黑马商城-微服务面试篇》,分布式架构项目,从开发到部署
java·微服务·架构
better_liang6 小时前
每日Java面试场景题知识点之-Spring Cloud微服务分布式事务解决方案
java·spring cloud·微服务·seata·面试题·分布式事务·tcc
一个帅气昵称啊6 小时前
.Net基于AgentFramework中智能体Agent Skill集成Shell命令实现小龙虾mini版
ai·c#·.net·openclaw
weixin_397578026 小时前
微服务 如何调试restful 接口
微服务