本文发表于 288 天前,其中的信息可能已经事过境迁
文章摘要

《诗经》有言:七月流火,九月授衣,这句话常被用来描绘夏秋交替、天气由热转凉的季节变化。西安的雨季,自六月下旬悄然而至、连绵不绝,不由地令人感慨:古人诚不欺我。或许,七月注定是个多事之“”,前有萝卜快跑及其背后的无人驾驶引发热议,后有特朗普在宾夕法尼亚州竞选集会时遇刺,更遑论洞庭湖决口、西二环塌方。杨绛先生说,成长就是学会心平气和地去面对这世界的兵荒马乱,可真正的战争“俄乌冲突”至今已经持续800多天。有时候,我不免怀疑,历史可是被诅咒了的时间?两年前的此时此刻,日本前首相安倍晋三遇刺身亡,我专门写过一篇文章《杂感·七月寄望》 。现在,回想起两人长达19秒的史诗级握手画面,一时间居然有种“一笑泯恩仇”的错觉。因为,从某种意义上来说,他们似乎成为了共患难的“战友”。雍正之于万历,如同特朗普之于肯尼迪,虽时过境迁,而又似曾相识,大概世间万物总逃不出某种循环。最近一个月,从 RAG 到 Agent,再到微软 GraphRAG 的爆火,诸如 Graph、NER、知识图谱等知识点再次被激活。我突然觉得,我需要一篇文章来整理我当下的思绪。

实现 Agent 以后

参照复旦大学的 RAG 综述论文实现 Advance RAG 以后,我开始将目标转向 Agent。一般来说,一个 Agent 至少应该具备三种基本能力:规划(Planning)、记忆(Memory)以及工具使用(Tool Use),即:Agent = LLM + Planning + Memory + Tool Use。如果说,使用工具是人类文明的起点,那么,Agent 则标志着大模型从 “说话” 进化到 “做事”。目前的 Agent 或者是说智能体,本质上都是将大模型视作数字大脑,通过反思、CoT、ReAct 等提示工程模拟人类思考过程,再通过任务规划、工具使用来扩展其能力的边界,使其能够感知和连接真实世界。从早期的 AutoGPT 到全球首个 AI 程序员智能体 Devin,人们对于 AI 的期望值,正肉眼可见地一路水涨船高。

Agent 的基本概念 Agent 的基本概念

目前,市场上主流新能源汽车的智驾系统都大多处于 L2 或 L3 级别,萝卜快跑则率迈进 L4 级别。尽管我可以理解这一发展趋势的必然性,可当我意识到碳基生命自身的偶然性,我想知道,那些可能导致成千上万的人失业的失业的科技创新,是否是显得过于残酷和冰冷?在2024年的上半年,我接触到了多种 Agent 产品,例如 FastGPTCozeDify 等等。这些产品基本都是基于工作流编排的思路,这实际上是一种对大型模型不稳定输出和多轮对话调用成本的妥协。受到过往工作经历影响,我对于工作流和低代码非常反感。因此,我坚信大模型动态地规划和执行任务的能力才是未来。在实现 Agent 的过程中,我参考 Semantic Kernel 的一个 PR 实现了一个支持 ReAct 模式的 Planner,这证明了我从去年开始接触大型模型时的种种想法,到目前为止基本上都是正确的。

当下生成式 AI 的优化方向 当下生成式 AI 的优化方向

我主张采用小模型结合插件的方式,推进 AI 服务的本地化,因为一味地追求参数规模或上下文长度,只会陷入永无休止的百模大战。在技术和成本之间,你必须要找到一个平衡点。例如,最近大火的 GraphRAG,知识图谱结合大模型的理念虽好,但构建知识图谱的成本相对较高,运行一个简单示例的费用大约在5到10美元左右。在实现 Agent 的过程中,我发现,使用阿里的 Qwen2-7B 模型完全可以支持任务规划以及参数提取,唯一的问题是 Ollama 推理速度较慢,尤其是在纯 CPU 推理的情况下。此外,目前的 Agent 的反思功能大多依赖于多轮对话,其效果易受上下文长度的影响。即便使用 OpenAIMoonshot 等厂商的服务,它们的 TPM/RPM 通常不会太高,导致公共 API 难以满足 Agent 的运行需求。如果增加接口调用间隔,无疑又会让屏幕前的用户失去耐心。因此,即便是在 token 价格越来越便宜的情况下,以任务为导向的 Agent,其 token 消耗量依然是一笔不小的开销。

Agent 如何思考和观察 Agent 如何思考和观察

一个名为爱因斯坦的智能体 一个名为爱因斯坦的智能体

在调试 Agent 的过程中,博主曾先后将 OpenAI 和 Moonshot 用至“欠费”,最终不得不转向更为经济的 DeepSeek。最近,有朋友同我抱怨,“召回精度是提升了,可生成答案时间同样变长了”。这个问题在 Agent 中同样存在,当大模型观察到当前结果差强人意时,会尝试使用不同的工具来解决问题,可往往是花费了时间和金钱,最后还是没能得到满意答案。归根到底,最关键的推理能力来自模型本身,提示词不过是锦上添花,你是可以持续地“反思”和“观察”,可如果面对的是完全未知的事物,这一切又有什么意义呢?以电商业务为例,其特点是数据链路长、牵涉多个微服务,这使得我无法同时满足强一致性和低延迟。这一道理同样适用于 Agent,不论是大模型动态规划的工作流,还是人工编排的工作流,如果你想要一个逻辑上闭环的流程,就要接受其可能耗费大量时间的现实。对于类似 RAG 这样的检索型的任务,你必须要检索精度和响应时间之间找到平衡点。

Agentic 比是不是 Agent 更重要 Agentic 比是不是 Agent 更重要

在为大模型接入了日期/时间、天气预报、新闻报道、搜索引擎、网络爬虫等工具以后,我突然感到一切索然无味,甚至有时会觉得,它不再像原来那样“开朗”,甚至变得不苟言笑起来。最终,它变成了一个合格的、帮助我连接真实世界的“工具人”。可大模型应该被这些词汇修饰吗?或许,这只是我的一厢情愿。因为从某种角度来看,这一切的元凶,在于我的外部知识“污染”了它的先验知识,无论我怎么努力,它并不会比市面上的 AI 助手强大多少。

Text2SQL 实践

OK,现在让我们讨论Text2SQL Text2SQL,这是将大型模型与关系型数据库连接起来的一种尝试。实现 Agent 后,你会发现 RAG 实际上是 Agent 中的一个工具,并且广义上的 RAG 并不仅仅局限于向量数据库,它还可以扩展到搜索引擎、知识图谱、第三方 API、各种数据源(网页/SQL/文档)以及聊天的上下文。这样,我们便会意识到,如果大模型可以从数据库中读取信息,它便掌握了世界上使用最广泛的数据源。在 RAG 应用中,文档中的图像、表格等可能不适合进行向量检索,而数据库中存储的结构化数据对大型模型更为友好。唯一的问题是,关系型数据库通常使用 SQL 来进行查询。因此,如果大模型能生成 SQL 语句,那么,从大模型到数据库的链路便被打通了。从生成式 AI 的角度来看,SQL 和 Python、C# 等编程语言类似,均属于代码生成的范畴,甚至可以说 SQL 更简单,因为 SQL中使用中关键字和指令更少。在这种背景下,Text2SQL 这种从自然语言转化为 SQL 的技术便应运而生。

一款以自然语言构建 SQL 的产品 一款以自然语言构建 SQL 的产品

接下来,我们来探究 Text2SQL 的具体实现步骤。目前,AI 应用落地的表里是,Agent 做面子,提示工程做里子。因此,一个直接的方法是将用户需求和数据库的 Schema 一并提交给大型模型,由模型来生成相应的 SQL 语句。对于那些热衷于模型微调和训练的“炼丹术士”,可以关注下 Hrida-T2SQL-3B-V0.1 这个专门为 Text2SQL 设计的小模型。对于像我这样的 NLP 民科,自然不会去挑战这种高难度任务。按照这个思路,第一步是获取数据库的 Schema,即:了解数据库里哪些表、每张表里都有哪些字段以及这些字段具体是什么类型。这些信息对于大型模型生成准确的 SQL 语句至关重要。这里以 MySQL 数据库为例进行说明:

SELECT t.TABLE_NAME,
  t.TABLE_COMMENT,
  c.COLUMN_NAME,
  c.COLUMN_COMMENT,
  c.DATA_TYPE,
  c.IS_NULLABLE
FROM INFORMATION_SCHEMA.TABLES t
LEFT JOIN INFORMATION_SCHEMA.COLUMNS c
  ON c.TABLE_NAME = t.TABLE_NAME
WHERE t.TABLE_SCHEMA = 'Chinook'

其中,Chinook 是数据库的名称,此时,我们就可以得到下面的结果:

获取数据库的 Schema 信息 获取数据库的 Schema 信息

接下来,我们只需按照第一列进行分组,便能获取每张表涵盖的字段。在此过程中,我同时提取了表和列的注释信息。因为在那些喜欢用拼音或是缩写命名的人面前,即使是大模型亦不免相形见绌。如果你的数据库表结构设计本就混乱不堪,不要说大模型,只怕是连神仙都难救。现在,让我们通过代码生成数据库的 Schema 信息:

// 获取表描述信息
private async Task<IEnumerable<TableDescriptor>> GetTableDescriptorsAsync(string databaseName)
{
    var sqlText =
        @"SELECT t.TABLE_NAME,
             t.TABLE_COMMENT,
             c.COLUMN_NAME,
             c.COLUMN_COMMENT,
             c.DATA_TYPE,
             c.IS_NULLABLE
        FROM INFORMATION_SCHEMA.TABLES t
        LEFT JOIN INFORMATION_SCHEMA.COLUMNS c
            ON c.TABLE_NAME = t.TABLE_NAME
        WHERE t.TABLE_SCHEMA = '{0}'";

    using var sqlClient = new SqlSugarClient(_connectionConfig);
    var rows = await sqlClient.Ado.SqlQueryAsync<dynamic>(string.Format(sqlText, databaseName));
    if (rows.Count == 0) return Enumerable.Empty<TableDescriptor>();

    return rows.GroupBy(x => x.TABLE_NAME).Select(g =>
    {
        return new TableDescriptor()
        {
            Name = g.ToList()[0].TABLE_NAME,
            Description = g.ToList()[0].TABLE_COMMENT,
            Columns = AsColumnDescriptors(g.ToList())
        };
    }).ToList();
}

// 获取列描述信息
private IEnumerable<ColumnDescriptor> AsColumnDescriptors(List<dynamic> rows)
{
    return rows.Select(x => new ColumnDescriptor()
    {
        Name = x.COLUMN_NAME,
        DataType = x.DATA_TYPE,
        Description = x.COLUMN_COMMENT,
        IsNullable = x.IS_NULLABLE == "YES"
    });
}

// 生成 Schema
private string GeneratorDatabaseSchema(IEnumerable<TableDescriptor> tableDescriptors)
{
    var stringBuilder = new StringBuilder();
    foreach (var tableDescriptor in tableDescriptors)
    {
        stringBuilder.AppendLine($"{tableDescriptor.Name}, {tableDescriptor.Description}");
        foreach (var columnDescriptor in tableDescriptor.Columns)
        {
            stringBuilder.AppendLine($" - {columnDescriptor.Name}, {columnDescriptor.DataType}, {columnDescriptor.Description}");
        }

        stringBuilder.AppendLine();
    }

    return stringBuilder.ToString();
}

最终,通过 GenerateDatabaseSchema() 方法,我们可以得到下面这样的结果:

为数据库生成 Schema 信息 为数据库生成 Schema 信息

接下来,就非常简单啦,我们只需要将这份 Schema 作为参数传入提示词模板即可,这份提示词可以在 这里 找到:

[KernelFunction]
[Description("根据用户的输入生成和执行 SQL 并返回 Markdown 形式的表格数据")]
public async Task<string> QueryAsync([Description("用户输入")] string input, Kernel kernel)
{
    var tableDescriptor = await GetTableDescriptorsAsync("Chinook");
    var databaseSchema = GeneratorDatabaseSchema(tableDescriptor);

    var promptTemplate = _promptTemplateService.LoadTemplate("Text2SQL.txt");
    promptTemplate.AddVariable("input", input);
    promptTemplate.AddVariable("schema", databaseSchema);

    var functionResult = await promptTemplate.InvokeAsync(kernel);
    var generatedSQL = functionResult.GetValue<string>().Replace("```sql", "").Replace("```", "");
    _logger.LogInformation("Generated SQL: {0}", generatedSQL);

    var queryResult = await ExecuteSQLAsync(generatedSQL);
    return queryResult;
}

当我们拥有可以 “思考” 的 Agent 以后,可以非常容易地为其扩展出 Text2SQL 能力。如下图所示,当用户给定一个查询时,首先是通过大模型生成 SQL 语句,其次是执行 SQL 语句返回结果,最后是大模型观察结果生成最终答案:

ReAct 模式驱动下的 Text2SQL ReAct 模式驱动下的 Text2SQL

以表格的形式向用户呈现答案 以表格的形式向用户呈现答案

当然,这种方案本质上依赖于模型的推理能力。对于常规查询,大模型通常可以做到游刃有余。然而,对于复杂的查询,如子查询、窗口函数、LeetCode 等,大模型往往开始显得力不从心,经常出现下面这几类问题:

  • 生成的 SQL 有语法错误
  • 使用了未定义的函数/变量
  • 查询结果重复

我个人认为,总体而言,Text2SQL 技术虽有不足,但谓瑕不掩瑜,它让大模型有了连接关系型数据库的可能。当人工智能成为一种通用能力和基础设置,它便不应该再成为普通人使用和学习的门槛。从这个角度出发,我们应该让世间万物都和大模型连接在一起,未来它应该会变成得像水、电、天然气一样不可或缺,大家觉得呢?

对效率的反思

作为程序员,或者说是技术人员,我们总是天然地追求着效率的最大化,甚至我们做过的每一个信息化的项目,都在加速着信息在互联网中的流动。可遗憾的是,我们是否掉进了由技术编织而成的名为“效率提升”的陷阱呢?正如我们有各种各样的聊天软件,可我们不见得就知道坐在你对面的那个人此刻在想些什么,甚至于我们在群组中被 @ 的时候,我们不见得有耐心读完、读懂全部信息,但我们还是要礼节性的打上一个:white_check_mark:。如今,无论天涯还是咫尺,都可以顺着网线来到你面前,这是“效率提升”这场运动带来的便利。可与此同时,在微博、小红书、抖音等社交媒体的影响下,这个世界亦开始变得不再那么真实。在一个程序员心目中,技术是没有立场、绝对正义的,可如果连正义都只是相对的,那么修饰正义的“绝对”就变得讽刺起来。于是,我们看到,在算法的驱使下,每个人被关进信息茧房,从此只能看到自己想看到的;于是,我们看到,在算法的算计下,外卖员的时间不断压缩,从此只能靠违章来争分夺秒;于是,我们看到,在敏捷的理论下,开发的周期越来越短,从此只能靠加班来卷赢同行……

当无人驾驶走进现实 当无人驾驶走进现实

我当然相信新的技术能带来新的机遇,但我们真正应该追求的效率,是那种可以让大多数人感到幸福的效率。百度发展 L4 级别无人驾驶,这件事情当然是正确的,甚至是非做不可的事情。可至少在当下这个时刻,提升效率并不是唯一正确的事情,就像生成式 AI 可以快速生成文本、图片甚至是视频,但这件事情并没有让我们从繁重的事务性工作中解脱出来,反而造成了人类世界的内卷,并且这种内卷完全没有赢的可能。以最新的 Claude 3.5 Sonnet 模型为例,它在某些情况下甚至可以媲美高级研究人员。这种效率提升对于普通智力水平的职场人来说,无异于降维打击。在当下这样一个充满危机感的职场环境下,像萝卜快跑这样的效率提升,除了断送掉“铁人三项”以外,我认为它并不会让更多的人感到幸福。一句话简单总结,事情是对的,但不合时宜。在职场中有一种潜规则,即:在工作中为员工设置各种障碍,从而使得每一个人都有事可做,这可以认为是职场中的不宣之秘。技术当然没有立场,有立场的从来都是人,如果一部分人坚持要提升效率、破旧立新,那只能说明在某个地方真的是有利可图。

本文小结

我有一个不太好的习惯:当感觉心中的想法不足以填满一篇博客时,我总想释放出脑中的所有念头,并试图将它们全部塞进同一篇文章中。这样做的后果是,我写出的文章题目与文章内容间只有松散的联系。如果你读到这里,依旧感到困惑,请允许我在此表达歉意。可能是因为我每天都在接触大量碎片化信息,难以在短时间内整理出清晰的知识体系。特别是在我实现 StepwisePlanner 这段时间,我突然觉得 LLM Agent 索然无味。尽管,我每天都能在微信群里看到大量 AI 资讯,整个行业看起来一派繁荣,但现实中频频传来的失业潮、AI行业无的放矢的普遍现象,再加上西安持续下了半个多月的雨,绵绵不绝,让我深切感受到现实与理想之间的鸿沟。因此,当我开始写这篇博客时,我想表达的远不止 Text2SQL 这样简单,可一时间我只能找到这样一个话题;正如我想做的是 Agent,可是目前我只能做到这种程度。当我向同事演示 Text2SQL 时,他安慰我说:“这不是你能力问题,现在模型就这样”。我开始有些分辨不出这句话的真假,人类是如此地渴望情绪价值,可如果 AI 满足了你所有的情绪需求,你真的会感到满足吗?人类明明拥有足够的上下文信息来推导出答案,但人们不见得就会主动说出来,不是吗?

赞赏博主
相关推荐 随便逛逛
Semantic Kernel × MCP:智能体的上下文增强探索 本文深入探讨了 MCP(模型上下文协议),由 Anthropic 设计的开放协议,它如同 AI 领域的 USB 接口,旨在通过统一接口解决大模型连接不同数据源和工具的问题。文章详细介绍了 MCP 的架构、核心角色、工作原理以及如何与 Semantic Kernel 集成,为 .NET 开发者提供高效接入社区 MCP 服务器的方法,减少重复性平台对接工作。此外,还展示了 MCP 在操作浏览器、访问文件系统等场景中的应用效果,并探讨了其局限性及未来发展方向。在 AI 技术快速发展的背景下,MCP 的出现为实现 AI 模型的 “万物互联” 提供了可能,值得开发者关注与探索。
浅议 CancellationToken 在前后端协同取消场景中的应用 本文深入探讨了在生成式AI领域中,前后端协同取消机制的重要性和实现方式。作者首先回顾了流式传输技术,然后通过 .NET 中的 CancellationToken 和 CancellationTokenSource 的使用示例,展示了如何在异步编程模型中处理任务取消请求。文章进一步分析了在 Web 服务中,如何通过 HttpContext.RequestAborted 属性感知客户端的取消请求,并给出了具体的代码示例。最后,作者讨论了在实际开发中,如何优化取消机制的实现,以提高代码的效率和可维护性。
评论 隐私政策