最近在不经意之间写了一个涉及EFCore事务性的bug。起因是某一个业务方法SubmitFlowAsync()中要复用一段流程引擎的接口,而这个业务类中除了这个业务方法之外的其它所有方法都和流程引擎无关,所以我下意识地从之前流程引擎的单元测试中拷贝了一段程序并稍加修改,变成了下面的代码:


// 通过依赖注入得到的字段  
private readonly IServiceScopeFactory _ssf;
  
// ...

// 一个业务方法
public async Task<FSharpResult<IEnumerable<FlowInvocationRecord>, IDomainErr>> SubmitFlowAsync(string definitionCode, string instanceCode)
{
    using var scope = this._ssf.CreateScope();
    var sp = scope.ServiceProvider;
    IFlowDefinitionRepo defRepo = sp.GetRequiredService<IFlowDefinitionRepo>(); // 流程引擎特有接口
    IFlowInstanceRepo instRepo = sp.GetRequiredService<IFlowInstanceRepo>();    // 流程引擎特有接口

    var q = from definition in this.LoadDefinitionAsync(definitionCode)
            from instance in this.CreateFlowInstance(instRepo, definition, Guid.NewGuid().ToString(), instanceCode)
            from records in this._flowInvoker.BeginAsync(sp, instRepo, definition, instance)
                .SelectError(e => {
                    IDomainErr err = new DummyErr("400", e.ToString() ?? this._localizer["流程启动错误"]);
                    return err;
                })
            select records;
    return await q;
}

调用代码类似于:

阅读剩余部分

场景和问题

最近把主力数据库都全部切换成了PostgreSQL,并用于它开发了物料管理系统demo。为了测试大量数据对性能的影响,我给本地的库存表灌入了几十万种物料和80万行库存项。在关键词模糊搜索、多表联查等环节,我对其性能表现都很满意;但是当我在盘库凭证中添加抽盘项,而分页查询库存的页码又超大时,在前端拿到分页结果竟然要1.2s左右。毫无疑问,这个时间是有优化空间的。

查询库存的SQL语句类似于:

SELECT t."Id", t."Amount", t."Batch", t."CreatedAt", t."ExpiredAt", t."ExternalCode", t."IsLocked", t."Locker", t."MaterialCode", t."MaterialId", t."Note", t."ProducedAt", t.xmin, t."Scope", t."Slot", t."StockKind", t."WarehouseId", m0."Id", m0."Code", m0."MeasureUnitId", m0."ShortName", m0."Specification", m0."Status", m1."Id", m1."Abbrev", m1."Description", m1."Name", m2."Id", m2."Code", m2."Name", m2."PlantId"
      FROM (
          SELECT m."Id", m."Amount", m."Batch", m."CreatedAt", m."ExpiredAt", m."ExternalCode", m."IsLocked", m."Locker", m."MaterialCode", m."MaterialId", m."Note", m."ProducedAt", m.xmin, m."Scope", m."Slot", m."StockKind", m."WarehouseId"
          FROM mm_inventory AS m
          WHERE m."StockKind" IN (1, 2, 3) 
          ORDER BY m."Id"
          LIMIT 10 OFFSET 790000
      ) AS t
      INNER JOIN mm_material AS m0 ON t."MaterialId" = m0."Id"
      INNER JOIN mm_material_measureunit AS m1 ON m0."MeasureUnitId" = m1."Id"
      INNER JOIN mm_warehouse AS m2 ON t."WarehouseId" = m2."Id"
      ORDER BY t."Id"

上面这条查询语句稍显啰嗦,不过通过分析,很容易发现瓶颈出现在最内层的子查询上:

阅读剩余部分

目前LLM非常强大,但是如果只将它们用于聊天补全、生成图像这类生成式的场景,无异于自断双臂。近年来Agent的出现,必将会让AI渗透到日常生活中的方方面面。

微软的semantic kernel关于Agent的介绍是:

An AI agent is a software entity designed to perform tasks autonomously or semi-autonomously by recieving input, processing information, and taking actions to achieve specific goals.

尽管我加粗表示了我个人认为重要的几个关键词,但是不得不说,这个介绍写得让人摸不着头脑。相比之下,LangChain中对Agent的定义就非常简洁:

Agent is a class that uses an LLM to choose a sequence of actions to take.

总之,我心中的Agent,是指发挥LLM的长处,让LLM作为推理引擎,规划执行外部动作。

本文简介

目前最火热的大模型当属OpenAIDeepSeek,尤其是网上关于使用OpenAI构建Agent的子类非常多,但是在当前这个时间节点,我认为还有还有些不如意的地方:

  • 并不是每个中国人都能使用常规信道获得openaikey,也不是每个人都有国外信用卡来付费。
  • 至于DeepSeek,除了系统繁忙网络超时问题之外,DeepSeek著名的R1推理模型并不官方支持function calling(参见 https://github.com/deepseek-ai/DeepSeek-R1/issues/9#issuecomment-2604747754),而截至今日,最新版的function calling尚不稳定。

注: DeepSeek 官方针对R1模型的函数调用给出了三种 walkaround


- 使用脚本解析模型输出到结构化格式(比如:“JSON”格式)
- 设计提示词工程来指定模型产生特定格式的输出
- 自定义包装器,来模拟函数调用

所以,目前的尴尬就在于,我不想等:“高老师,我太想进步了”。

本文文章使用Ollama+QWen2.5来驱动。

  • Ollama的意义在于我们可以本地部署,避免依赖外部网络环境,这在涉密或者离线场合意义非凡。
  • QWen2.5的意义在于对中文支持非常好,不像是llama3.2那样,动不动就自动转换成英语,显得非常混乱。

阅读剩余部分

信息量、信息熵、交叉熵是非常重要的数学概念。它们非常重要,相关书籍和资料也很多,不过都不够友好——世上的事情总是如此,你尚不理解的,对于你而言太难;而你已然理解的,对你而言又太过简单。

所以很难有适合所有人的学习资料。这是我以程序员的视角,向自己介绍这几个相关概念。

这篇笔记题目起得太大,不是面向程序员的直观解释,而是面向我这个程序员的直观解释。

信息量

考虑一个抛硬币的游戏:抛出一个硬币,问表示这个事件发生的结果,最多需要几个比特?显然,一个比特位就够了(\(2^1=2\)),比如规定:

  • 1: 代表正面
  • 0: 代表反面

让我们整理一下这里的术语:

  • 随机事件:表示一次抛硬币的事件,要么正面朝上,要么反面朝上,只能是其中之一。
  • 随机变量:表示一个变量,其值是各个随机事件。对于抛硬币来说,可能是正面朝上,也可能是反面朝上。
  • 编码:用数字来对随机事件进行唯一编号。
  • 比特:计算机术语,一个存储位,可以表示两种情况。可以用灯来比喻。

我们重新描述一下上面的问题:

用随机变量\(X\)表示一次抛硬币的结果。我们可以用1来编码正面朝上这个随机事件,用0来编码反面朝上这个事件。需要分配1个比特位(一盏灯)就足够了。
如果硬币被人做了手脚,必然正面朝上,那么对于这种必然事件,我们连一个比特位都不需要分配,即需要0个比特。
同理,如果硬币被人做了手脚,必然反面朝上,我们也不需要分配任何比特位,即需要0个比特。。

如果你的数学直觉足够好,你可能会意识到,要编码上面丢硬币的结果,需要的比特数和随机事件发生的概率有关:

  • 当硬币是正面朝上和反面朝上的概率均是50%时,我们需要1个比特;
  • 而当正面朝上是100%的概率时,我们需要0个比特;
  • 而当反面朝上是100%的概率时,我们也需要0个比特;
  • 如果我们把比特从整数扩展到实数,当正面朝上和反面朝上的概率取其它值时,需要几个比特来编码结果?从直觉上,我们可以猜测,需要的比特数应该介于\((0,1)\)之间。

甚至,我们可以猜测:

  • 当概率构成是(0.5, 0.5)时,我们所需要的1个比特有一半分给了编码正面朝上、另一半分给了反面朝上。
  • 而当概率构成是(1.0, 0.0)时,正面朝上是必然事件,无需比特进行编码;反面朝下也是必然事件,也无需比特进行编码。
  • 而当概率构成是(0.0, 1.0)时,正面朝下是必然事件,无需比特进行编码;反面朝上也是必然事件,也无需比特进行编码。
  • 当概率构成是(p, 1-p)时,这个需要的比特量里有一部分被分给了对正面朝上编码,另一部分属于反面朝上进行编码。至于这个构成是多少,我们留待下面进行更多的探究。

再考虑需要的比特量稍大一点的情况。已知有一个随机整数,取值范围是\([1,16]\)。那么表示这个数到底是多少,需要几个比特?显然,\(2^4=16\),也就是需要4个比特。这个问题也可以换个角度观察:由于有4个比特位,如果逐一确定这里的四个比特位分别是多少,我们共需要测试四次。

阅读剩余部分

如何绘制PR曲线?

基本思想

二元预测函数的输出是一个得分。从预测得分到判定是否属于某类,还需要结合阈值来完成。比如大于某个阈值,就认为是某个类。调节阈值,会影响预测的结果类别,最终会影响精准率和召回率。在直觉上,精准率和召回率在一定程度上会呈现负相关关系——漏杀低了,容易过杀;过杀低了,又容易漏杀。我们想把这个关系量化表示,一个简单办法就是绘制P-R曲线。

示例

假设我们有一个二元分类问题,我们对每一行样本都进行了预测,并给出了预测得分:

序号 真实值 预测分数
1    0    0.1
2    1    0.4
3    1    0.35
4    0    0.8
5    1    0.9
6    0    0.2
7    1    0.5
8    0    0.3
9    0    0.6
10    1    0.85

既然要调节阈值来观测输出,不妨把上面各行先按预测得分来排列:

阅读剩余部分