惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

F
Fortinet All Blogs
S
Secure Thoughts
月光博客
月光博客
美团技术团队
雷峰网
雷峰网
Exploit-DB.com RSS Feed
Exploit-DB.com RSS Feed
奇客Solidot–传递最新科技情报
奇客Solidot–传递最新科技情报
N
News and Events Feed by Topic
freeCodeCamp Programming Tutorials: Python, JavaScript, Git & More
Forbes - Security
Forbes - Security
W
WeLiveSecurity
P
Proofpoint News Feed
阮一峰的网络日志
阮一峰的网络日志
爱范儿
爱范儿
G
GRAHAM CLULEY
cs.AI updates on arXiv.org
cs.AI updates on arXiv.org
AI
AI
Last Week in AI
Last Week in AI
Google Online Security Blog
Google Online Security Blog
Schneier on Security
Schneier on Security
云风的 BLOG
云风的 BLOG
Threat Intelligence Blog | Flashpoint
Threat Intelligence Blog | Flashpoint
Recent Announcements
Recent Announcements
Webroot Blog
Webroot Blog
T
Tor Project blog
Cisco Talos Blog
Cisco Talos Blog
N
News and Events Feed by Topic
罗磊的独立博客
The Register - Security
The Register - Security
Blog — PlanetScale
Blog — PlanetScale
T
Threat Research - Cisco Blogs
博客园 - 【当耐特】
Apple Machine Learning Research
Apple Machine Learning Research
人人都是产品经理
人人都是产品经理
T
The Exploit Database - CXSecurity.com
www.infosecurity-magazine.com
www.infosecurity-magazine.com
B
Blog
腾讯CDC
Microsoft Azure Blog
Microsoft Azure Blog
酷 壳 – CoolShell
酷 壳 – CoolShell
H
Hacker News: Front Page
Application and Cybersecurity Blog
Application and Cybersecurity Blog
Engineering at Meta
Engineering at Meta
Latest news
Latest news
IT之家
IT之家
D
DataBreaches.Net
博客园 - 司徒正美
N
Netflix TechBlog - Medium
V
V2EX
钛媒体:引领未来商业与生活新知
钛媒体:引领未来商业与生活新知

元视角

.NET 生态下的 Agent 框架选型:从 ReAct 到原生推理 - 元视角 从「能用」到「好用」:LLM 流式响应实现方式的探索之路 - 元视角 当我用 2000 条聊天记录,让 AI 为我画一幅自画像 - 元视角 基于 Supabase 的 AI 应用开发探索 - 元视角 微博 × MCP:社交媒体新玩法解锁 - 元视角 四点钟海棠花未眠 - 元视角 Semantic Kernel × MCP:智能体的上下文增强探索 - 元视角 基于 K-Means 聚类分析实现人脸照片的快速分类 - 元视角 容器技术驱动下的代码沙箱实践与思考 - 元视角 浅议 CancellationToken 在前后端协同取消场景中的应用 - 元视角 Semantic Kernel 视角下的 Text2SQL 实践与思考 - 元视角 关于 ChatGPT 的流式传输,你需要知道的一切 - 元视角 RAG 的是与非、Rewrite 和 Rerank - 元视角 使用 EFCore 和 PostgreSQL 实现向量存储及检索 - 元视角 基于 LLaMA 和 LangChain 实践本地 AI 知识库 - 元视角 使用 llama.cpp 在本地部署 AI 大模型的一次尝试 - 元视角 如何为 Git 配置多个 SSH Key - 元视角 C# 使用 LibUsbDotNet 实现 USB 设备检测 - 元视角 基于 C# 实现样式与数据分离的打印方案 - 元视角 基于 SVG 的图形交互方案实践 - 元视角 前端视频播放技术概览 - 元视角 温故而知新,再话 Python 动态导入 - 元视角 后 GPT 时代,NLP 不存在了? - 元视角 视频是不能 P 的系列:使用 Milvus 实现海量人脸快速检索 - 元视角 GDI+下字体大小自适应方案初探 - 元视角 小爱音箱集成 ChatGPT 的不完全教程 - 元视角 程序员视角下的三体世界随想 - 元视角 关于 Docker 容器配置信息的渐进式思考 - 元视角 在 Docker 容器内集成 Crontab 定时任务 - 元视角 为你的服务器集成 LDAP 认证 - 元视角 似花还似非花 - 元视角 视频是不能 P 的系列:使用 Dlib 实现人脸识别 - 元视角 浅议分布式链路追踪与日志的整合 - 元视角 关于 Git 大文件上传这件小事 - 元视角 .NET 进程内队列 Channel 的入门与应用 - 元视角 使用 Fody 实现 .NET 的静态编织 - 元视角 .NET Core + ELK 搭建可视化日志分析平台(下) - 元视角 聊一聊前端图片懒加载背后的故事 - 元视角 支持外部链接跳转的 Vue Router 扩展实现 - 元视角 视频是不能 P 的系列:OpenCV 和 Dlib 实现表情包 - 元视角 不得不说的 ASP.NET Core 集成测试 - 元视角 再议 DDD 视角下的 EFCore 与 领域事件 - 元视角 Vue.js 前端项目容器化部署实践极简教程 - 元视角 再见,人间四月天 - 元视角 Python 图像风格化迁移助力画家梦想 - 元视角 利用 ASP.NET Core 中的标头传播实现分布式链路追踪 - 元视角 利用 gRPC 实现文件的上传与下载 - 元视角 七种武器:延迟队列的原理和实现总结 - 元视角 gRPC 流式传输极简入门指南 - 元视角 Envoy 集成 Jaeger 实现分布式链路追踪 - 元视角 浅议非典型 Web 应用场景下的身份认证 - 元视角 gRPC 借助 Any 类型实现接口的泛化调用 - 元视角 分布式丛林探险系列之 Redis 集群模式 - 元视角 分布式丛林探险系列之 Redis 主从复制模式 - 元视角 通过 Python 预测 2021 年双十一交易额 - 元视角 gRPC 搭配 Swagger 实现微服务文档化 - 元视角 SSL/TLS 加密传输与数字证书的前世今生 - 元视角 使用 Python 自动识别防疫健康码 - 元视角 你不可不知的容器编排进阶技巧 - 元视角 ASP.NET Core 搭载 Envoy 实现 gRPC 服务代理 - 元视角 再话 AOP,从简化缓存操作说起 - 元视角 ASP.NET Core 搭载 Envoy 实现微服务身份认证(JWT) - 元视角 ASP.NET Core 搭载 Envoy 实现微服务的监控预警 - 元视角 ASP.NET Core 搭载 Envoy 实现微服务的反向代理 - 元视角 ASP.NET Core gRPC 打通前端世界的尝试 - 元视角 EFCore 实体命名约定库:EFCore.NamingConventions - 元视角 ASP.NET Core gRPC 集成 Polly 实现优雅重试 - 元视角 ASP.NET Core gRPC 健康检查的探索与实现 - 元视角 ASP.NET Core gRPC 拦截器的使用技巧分享 - 元视角 SnowNLP 使用自定义语料进行模型训练 - 元视角 使用 HttpMessageHandler 实现 HttpClient 请求管道自定义 - 元视角 ABP vNext 的实体与服务扩展技巧分享 - 元视角 ABP vNext 对接 Ant Design Vue 实现分页查询 - 元视角 源代码探案系列之 .NET Core 跨域中间件 CORS - 元视角 源代码探案系列之 .NET Core 限流中间件 AspNetCoreRateLimit - 元视角 源代码探案系列之 .NET Core 并发限制中间件 ConcurrencyLimiter - 元视角 通过 EmbededFileProvider 实现 Blazor 的静态文件访问 - 元视角 低代码,想说爱你不容易 - 元视角 记一次失败的 ThoughtWorks 面试经历 - 元视角 从 C# 1.0 到 C# 9.0,历代 C# 语言特性一览 - 元视角 通过 Python 分析 2020 年全年微博热搜数据 - 元视角 基于 Python 和 Selenium 实现 CSDN 一键三连自动化 - 元视角 使用多线程为你的 Python 爬虫提速的 N 种姿势,你会几种? - 元视角 实现网页长截图的常见思路总结 - 元视角 温故而知新,由 ADO.NET 与 Dapper 所联想到的 - 元视角 视频是不能 P 的系列:OpenCV 人脸检测 - 元视角 作为技术宅的我,是这样追鬼滅の刃的 - 元视角 使用 Python 抽取《半泽直树》原著小说人物关系 - 元视角 厉害了!打工人用 Python 分析西安市职位信息 - 元视角 使用 dotTrace 对 .NET 应用进行性能分析与优化 - 元视角 一道 HashSet 面试题引发的蝴蝶效应 - 元视角 基于选项模式实现.NET Core 的配置热更新 - 元视角 Dapper.Contrib 在 Oracle 环境下引发 ORA-00928 异常问题的解决 - 元视角 .NET Core 中对象池(Object Pool)的使用 - 元视角 利用 MySQL 的 Binlog 实现数据同步与订阅(下):EventBus 篇 - 元视角 利用 MySQL 的 Binlog 实现数据同步与订阅(中):RabbitMQ 篇 - 元视角 利用 MySQL 的 Binlog 实现数据同步与订阅(上):基础篇 - 元视角 记一次从已损坏的 Git 仓库中找回代码的经历 - 元视角 .NET Core 原生 DI 扩展之属性注入实现 - 元视角 .NET Core 原生 DI 扩展之基于名称的注入实现 - 元视角
温故而知新:后端通用查询方案的再思考 - 元视角
飞鸿踏雪 · 2024-09-23 · via 元视角

最近,我一直在体验 Cursor 这款产品,与先前的 CodeGeex通义灵码“插件类” 产品相比,Cursor 在产品形态上更接近 Github Copilot。在多项测评中,Cursor 甚至一度超越了 Github Copilot。尽管我没有体验过 Github Copilot,但从用户体验的角度来看,Cursor 基于 VS Code 进行了深度定制。除了基础的代码自动补全功能外,它还可以允许你从原型图生成代码、将整个工程作为 Codebase、一键应用代码到本地。最令我印象深刻的是,它指导我完成了一个 Vue 的小项目,从零开始。诚然,“幻觉” 的存在让它在 Vue 2 和 Vue 3 之间反复横跳,其编程能力的提升主要得益于 Claude 3.5 系列模型,可我还是像《三体》中的杨冬一样感到震惊:物理学不存在了,那前端呢?有人说,程序员真正的护城河是沟通能力,因为执行层面的工作可以交给 AI。实际上,我并不担心 AI 取代人类,我更倾向于与 AI 沟通和合作,你可能想象不到,这篇文章中的思考正是来自于我和 Claude 老师的日常交流。

CRUD Boys 的日常

程序员普遍喜欢自嘲,以博主为例,作为一名后端工程师,我的日常工作主要就是 CRUD,因此,你可以叫我们 CRUD Boys。鲁迅先生曾作《自嘲》一诗,“破帽遮颜过闹市,漏船载酒泛中流”。面对软件世界里里的复杂性和不确定性,如果没有乐观的心态和耐心,哪怕是最基础的 CRUD,你不见得就能做到得心应手。你可能听说过这样一句话,“上岸第一剑,先斩意中人”,AI 领域的第一把火,永远烧向程序员自己,自打一众 AI 辅助编程工具问世以来,各种程序员被 AI 取代的声音不绝于耳,甚至 Cursor 可以在 45 分钟内让一个 8 岁小孩搭建出聊天网站,更不必说,在 OpenAI 发布全新的 o1 模型后,很多人觉得连提示工程、Agent 这些东西都不存在了。其实,代码生成、低代码/无代码相关的技术一直都存在,在很久以前,我们就在通过 T4 模板生成业务代码,自不必说各种代码生成器。截止到目前,Excel 依然是这个地球上最强大的低代码工具,可又有谁能掌握 Excel 的全部功能呢?

你猜用 Cursor 写一个这样的页面需要多久? 你猜用 Cursor 写一个这样的页面需要多久?

退一步讲,即使的最简单的 CRUD,虽然业务的推进会不断地演化出新的问题。譬如,当你为了加快查询效率引入了缓存,你需要去解决数据库和缓存一致性、缓存失效等问题;当你发现数据库读/写不平衡引入读写分离、分库分表,你就需要去解决主从一致、分布式事务、跨库查询等问题;当你发现单点性能不足引入了多机器、多线程,你需要去解决负载均衡、线程同步等问题……单单一个查询就如此棘手,你还会觉得后端的 CRUD 简单吗?我承认,后端的确都是 CRUD,可在不同的维度上这些 CRUD 并不完全相同,譬如,分布式的相关算法如 PaxosRaft 等,难道不是针对分布式环境中的节点做 CRUD 吗?可此时你还会觉得它简单吗?Cursor 的确可以帮你生成代码,但真正让它出圈的是背后的 Claude 模型。我始终相信某位前辈曾经讲过的话:“没有银弹”,在软件行业里,复杂度永远不会消失,它只会以一种新的方式出现。如果你觉得 CRUD 简单,或许是你从未接触过那些千姿百态的查询接口:

// OData 风格
Products?$filter=Category eq 'Electronics'&$orderby=Price desc&$top=10&$skip=20

// GraphQL 风格
{
  products(category: "Electronics", first: 10, skip: 20, orderBy: {field: PRICE, direction: DESC}) {
    name
    price
  }
}

// RESTful 风格
products?category=Electronics&sort=-price&limit=10&offset=20
products?filter[category]=Electronics&sort=-price&page[limit]=10&page[offset]=20

从 Gridify 中得到的启发

Gridify 是一个卓越的动态 LINQ 库,它可以将字符串转化为 LINQ 查询。因此,它非常适合处理分页、过滤、排序等问题。如以下代码示例所示,你只需要在控制器方法中添加一个 GridifyQuery 类型的参数,即可实现一个通用的分页查询接口,可以说是非常的 Amazing 啊!👌

[HttpGet]
public Paging<Product> GetPagingProducts([FromQuery]GridifyQuery query) {
    var queryable = _context.Products.AsQueryable();
    return queryable.Gridify(query);
}

事实上,Gridify 为 IQueryable 接口提供了丰富的扩展方法,这使得我们可以如此优雅地实现分页查询功能。此时,我们可以使用类似下面这样的语法来查询产品信息:

// 查询品牌中含有 Nick 的产品
api/products?page=1&pageSize=10&filter=brand=*Nike

// 查询价格大于或等于 10 元的产品
api/products?page=1&pageSize=10&filter=price>=10

// 查询名称以 Nick 开头的产品
api/products?page=1&pageSize=10&filter=name=^Nick

// 查询品牌中含有 Nick 的产品,并按 SKU 降序排列
api/products?page=1&pageSize=10&filter=brand=*Nike&orderBy=sku desc

你可以理解为,Gridify 定义了一套过滤和排序的语法,这些语法将会被解析、编译为表达式,并最终应用到 IQueryable 接口上面。对于 .NET 开发者来说,常见的做法是:使用 Where() 筛选数据,使用 Skip() 和 Take() 实现分页查询,以及使用 OrderBy()、OrderByDescending()、ThenBy() 和 ThenByDescending() 对数据进行排序。目前,Gridify 支持的操作符如下所示:

Gridify 中定义的操作符 Gridify 中定义的操作符

这个方案在某种程度上与我过去开源的项目 DynamicSearch 非常相似,都是通过构建表达式实现动态查询。事实上,之前已经有一个名为 System.Linq.Dynamic 的项目专门解决这类动态查询问题。唯一的挑战是,这些方案对前端同事来说过于复杂了,即便 Gridify 中非常贴心的提供了前端实现。当然,一个更要的原因是,我更倾向于避免解析字符串信息。那么,是否有更好的解决方案呢?诸如 Java 程序员在 XML 里写 SQL 的做法,窃为我所不取也!

一个通用查询方案的实现

首先,我们参照 GridifyQuery 定义一个泛型类 QueryParameter:

class QueryParameter<TEntity,TFilter> where TFilter : class, IQueryableFilter<TEntity> {
    public int PageIndex { get; set; }
    public int PageSize { get; set; }
    public TFilter Filter { get; set; }
    public string SortBy { get; set; }
    public bool IsDescending { get; set; }
}

其中,参数 TFilter 需要满足一定的约束条件,即:实现 IQueryableFilter 接口,该接口定义如下:

interface IQueryableFilter<TEntity> {
    ISugarQueryable<TEntity> Apply(ISugarQueryable<TEntity> queryable);
}

博主这里使用的 ORM 是 SqlSugar,因此,这个接口需要依赖 ISugarQueryable 接口,如果你使用 EntityFramework,可以将其替换为 IQueryable。当我们需要对特定数据进行筛选时,我们只需要为其定义一个或者多个过滤条件即可。例如,通过下面的 LlmModelQueryableFilter 类,即可完成对大模型信息的检索:

class LlmModelQueryableFilter : IQueryableFilter<LlmModel> {
    public string ModelName { get; set; }
    public int? ModelType { get; set; }
    public int? ServiceProvider {  get; set; }

    public ISugarQueryable<LlmModel> Apply(ISugarQueryable<LlmModel> queryable) {
        if (!string.IsNullOrEmpty(ModelName))
            queryable = queryable.Where(x => x.ModelName.Contains(ModelName));

        if (ModelType.HasValue)
            queryable = queryable.Where(x => x.ModelType == ModelType);

        if (ServiceProvider.HasValue)
            queryable = queryable.Where(x => x.ServiceProvider == ServiceProvider);

        return queryable;
    }
}

通常情况下,你只需要继承 CrudBaseController,并将其与实体类关联起来即可:

[Route("api/[controller]")]
[ApiController]
class LlmModelController : CrudBaseController<LlmModel,LlmModelQueryableFilter> {
    private readonly IRepository<LlmModel> _llmModelRepository;
    public LlmModelController(CrudBaseService<LlmModel> crudBaseService, IRepository<LlmModel> llmModelRepository) : base(crudBaseService) {
        _llmModelRepository = llmModelRepository;
    }
}

此时,你将得到一组 RESTful 风格的 CRUD 接口,如下图所示:

自动解锁一组 RESTful 风格的 CRUD 接口 自动解锁一组 RESTful 风格的 CRUD 接口

其中, api/LlmModel/paginateapi/LlmModel/list 具有相同的过滤条件,真正做到了 “一次编写,到处运行”。关于 CrudBaseController 的具体实现,我认为并不复杂,关键代码片段展示如下:

// 分页查询
async Task<PagedResult<T>> PaginateAsync<TQueryFilter>(
    QueryParameter<T, TQueryFilter> queryParameter, 
    ISugarQueryable<T> queryable = null
)
    where TQueryFilter : class, IQueryableFilter<T> {
    queryable = queryable ?? base.AsQueryable();

    if (queryParameter.Filter != null)
        queryable = queryParameter.Filter.Apply(queryable);

    var total = await queryable.CountAsync();

    queryable = queryable.Skip((queryParameter.PageIndex - 1) * queryParameter.PageSize).Take(queryParameter.PageSize);

    if (!string.IsNullOrEmpty(queryParameter.SortBy))
        queryable = queryable.OrderByPropertyName(queryParameter.SortBy, queryParameter.IsDescending ? OrderByType.Desc : OrderByType.Asc);

    var list = await queryable.ToListAsync();
    return new PagedResult<T> { TotalCount = total, Rows = list };
}

// 列表查询
Task<List<T>> FindListAsync<TQueryFilter>(
    TQueryFilter filter, 
    ISugarQueryable<T> queryable = null
) 
    where TQueryFilter : class, IQueryableFilter<T> {
    queryable = queryable ?? base.AsQueryable();
    if (filter != null) queryable = filter.Apply(queryable);

    return queryable.ToListAsync();
}

大家可能已经注意到,代码本身并没有变,但是它解决了一个关键问题:如何让整个控制器层变得整洁。回想一下,我们过去是如何处理这类问题的?是不是经常不断地向控制器添加参数?虽然我可能只是一个 CRUD Boy,可我并不愿意就此止步,能从这个再熟悉不过的话题中 “温故而知新”、查漏补缺,这是我写这篇博客的主要目标。让我们沿着这个思路继续探索,此时,我们会发现这里的过滤条件并不符合我们的预期:

Swagger 中展示的分页查询接口 Swagger 中展示的分页查询接口

具体来说,博主期待的查询参数应该是下面这种格式:

api/LlmModel/paginate?pageIndex=1&pageSize=10&modelName=llama3&modelType=0

事实上,在真实的请求中,你将会看到的下面这种格式的查询参数:

api/LlmModel/paginate?pageIndex=1&pageSize=10&filter.modelName=llama3&filter.modelType=0

在某个瞬间,你是否会对这个格式感到困惑呢?实际上,这两种格式都是合理的,前者被称为平铺格式,而后者则被称为嵌套格式。其中,平铺模式对前端友好,缺点是当参数较多时可能会造成混淆,无法区分出哪些是过滤条件;嵌套模式对后端友好,可以直接映射到 QueryParameter 参数的 Filter 属性上,缺点是可能与现有组件不兼容,需要前端在传参时做特殊处理。考虑到,无论是 HTTP 协议还是 RESTful 规范,在这一类问题均未做出强制性约束。因此,在实际工作中,你会看到五花八门的实现方式,如以下代码所示:

// 使用 JSON 编码的查询参数
products?query={"page":{"index":1,"size":10},"sort":{"field":"price","direction":"desc"},"filter":{"name":"laptop","category":"electronics"}}

// 使用组合式的过滤条件
products?page=1&size=10&sort=price&order=desc&filters=name:laptop;category:electronics

结合前文可知,ODataGridify 都选择了组合式的过滤条件写法,而我们期望的是平铺格式的过滤条件写法。由此可见,单单是一个查询就有这么多种写法,秦始皇 “书同文”“车同轨”、统一度量衡的意义在这一刻终于找到了答案,可是话说回来,正是这种灵活性让我们有了发挥的空间,不是吗?那么,该如何解决这个问题呢?这里的方案是实现一个自定义的 IModelBinder,从而改变 ASP.NET Core 模型绑定的默认行为,关键代码如下:

TFilter BindFilter(ModelBindingContext bindingContext) {
    var filter = new TFilter();
    var filterType = typeof(TFilter);
    var properties = filterType.GetProperties();

    foreach (var property in properties) {
        var key = property.Name.ToLower();
        if (!ReservedParameters.Contains(key)) {
            // 同时兼容平铺型以及嵌套型参数
            // ?pageIndex=1&pageSize=10&name=xxx&age=xxx
            // ?pageIndex=1&pageSize=10&filter.name=xxx&filter.age=xxx
            var value = bindingContext.ValueProvider.GetValue(key).FirstValue;
            if (value == null) {
                value = bindingContext.ValueProvider.GetValue($"filter.{key}").FirstValue;
            }
            if (value != null) {
                object convertedValue = null;
                var targetType = Nullable.GetUnderlyingType(property.PropertyType) ?? property.PropertyType;

                if (targetType.IsEnum) {
                    convertedValue = Enum.Parse(targetType, value, true);
                } else if (targetType == typeof(bool) && bool.TryParse(value, out var _)) {
                    convertedValue = bool.Parse(value);
                } else if (targetType == typeof(DateTime) && DateTime.TryParse(value, out var _)) {
                    convertedValue = DateTime.Parse(value);
                } else {
                    if (!string.IsNullOrEmpty(value.ToString())) 
                        convertedValue = Convert.ChangeType(value, targetType);
                }

                property.SetValue(filter, convertedValue);
            }
        }
    }

    return filter;
}

可以注意到,本质上就是对两种类型的查询参数做了兼容,这样做的好处是,现在前后端都可以在各自的舒适区愉快工作,我认为这种效率的提升是有效提升,因为它真正地改善了生产关系,让前后端都能都能从中受益。相反,一味地提升效率而不去改善生产关系,永远都只会造成更多的内卷。这个方案是由我和 Claude 老师一起完成的,我觉得它还有进一步改善的空间,因为现在 IQueryableFilterISugarQueryable 强耦合,这显然不是我们希望看到的结果。当你使用 EntityFramework 时,你会希望这里变成 IQueryable;当你使用 MongoDB 时,你会希望这里变成 FilterDefinitionBuilder。所以,我认为这里还可以再引入一个中间层,你觉得呢?

本文小结

回顾整个探索过程,我不禁莞尔,从在Gridify 中寻找灵感,到亲自动手设计查询方案,再到深入思考参数传递的细节,对我而言,这个过程中是在一次次的思考和实践中,逐渐接近问题的本质。说到底,我们讨论的并不仅仅是一个查询方案,而是如何在灵活性和规范性之间寻找平衡。这个问题,恐怕从软件开发诞生以来就一直存在,而今天我们依旧在探索。也许,正是这种永无止境的探索,构成了编程的真正魅力。AI 无疑会给程序员带来不小的冲击,可仔细想想,它不过是我们工具箱中的新成员,就像我们并不会因为有了电动螺丝刀,就忘记如何使用普通螺丝刀。作为程序员,我们不应该局限在代码生成这一个点,而是要着眼于解决更复杂的问题、改善生产关系。本文探索的通用查询方案,表面上看是一个技术问题,实则触及了软件工程中的普适性难题,即:灵活性与规范性的取舍。我承认,AI 是可以帮我们快速生成各种代码,可未来的软件开发,决不会仅仅是代码的简单堆砌,为什么我们的目标不能是更高层次的系统设计和问题解决?就像这篇文章里提出的方案并非完美无瑕,它依然有改进和优化的空间。所以,请不要放弃你的主观能动性,让 AI 辅助你思考,而不是替你思考