过去两周间,笔者开发了一些基于 LLM 的玩具项目。第一个项目是源码问答机器人,它通过 RAG 获得源码片段,并以此回答用户的问题;第二个项目是数据库问答机器人,它在需要数据时,可以自行产生 SQL 语句,交由 postgresql 执行并获得结果,不断重复此过程,直到可以推理出答案。显然,这两个项目中,前者是一个 workflow,后者是一个 agent。
LLM 应用与传统 CRUD 应用的区别,大致有以下几点:(一)LLM 应用往往强调用户交互,因此在前端上需要十分用心;(二)LLM 的输出常常超出预计,且很多应用需要调用远程资源,各种不确定因素叠加在一起,需要我们尽量提升程序的鲁棒性;(三)如果说传统 CRUD 是以数据为中心的,则 LLM 应用是以 workflow/agent 流程为中心的。workflow/agent 的流程最好在早期就固化,然后以此为基础设计交互逻辑、界面等。
现在,两个项目已经上线,笔者也该写一篇文章,与读者分享这两周踩到的坑、收获的经验。请读者在评论区指正。
技术栈选型
长话短说,笔者选择的技术栈是:
- 后端:Flask
- 前端:React、Next.js(App Router)
- 数据库:PostgreSQL
- IDE:JetBrains 全家桶
LLM 应用对后端选型并无多少要求,笔者选择 Flask 是因为比较熟悉。但前端则并不如此:LLM 调用时间很长,如果不采用流式返回,则用户体验较差;若选用流式返回,则我们很需要现代前端,来帮助我们减少工作量。
笔者在若干年之前学习过 Vue,但早已忘得干净。上周重新学习前端,选择了 React。React 的哲学是纯函数式组件、数据单向传递,Vue 的哲学是双向数据绑定。可能接触过设计模式的人士会倾向于 React。
数据库的选型是由项目需求决定的。如果项目始终在访问非结构化的数据,则可能非关系型数据库更合适。不过笔者的项目中,数据基本上是结构化的,选择 pgsql 一是因为熟悉,二是因为 pgvector 扩展。这个扩展可以让 pgsql 承担起向量数据库的工作。
笔者建议 IDE 不选 Cline。最主要的考虑是,AI 产出的代码一般会比较冗长——在开发某模块时,笔者预估只要 60 行即可实现,但 Cline 足足写了 250 行,最后人工删减到 79 行。AI 编写的冗杂代码主要体现在注释、合法性检查、违背 DRY 原则的实现等,这个问题可以通过 prompt 缓解,但用起来总是不够顺手。最核心的问题是,AI 写的代码缺乏宏观设计、缺乏抽象。
举一个例子。某项目的 Cline prompt 如下:
**你应当使用中文与用户交互。**
本项目是一个刷题网站。数据库里面有若干个题库,每个题库内有若干题目,类型有选择题、多项选择题、判断题(判断题也当作选择题处理)。
技术栈:flask、fomantic-ui
具有如下功能:
- 看题模式。用户点进题库,可以逐题看题干、答案和解析。
- 刷题模式。用户点进题库,产生一个刷题事件。事件创建时,平台决定用户要刷的题目和顺序(随机顺序),用户按照顺序做题。每做完一题,立即告知用户正确答案。
有一个特性:当用户正确做出一道题目 k 次(目前k=4)之后,它对于这个用户就是“已掌握的题目”,无需再刷。当然,用户也可以在看题界面把已掌握的题目重新标为未掌握(即“遗忘”)。为了实现这个特性,你需要维护一张表,记录每个用户对于每道题的正确计数、错误计数、以及专用于“已掌握”特性的计数器。用户选择遗忘一道题时,将特殊计数器重置为0.
系统有三个入口程序:init_db.py用于初始化数据库(若有旧数据,也要抹除);load_excel.py用于导入数据;app.py 用于启动 flask 服务。
.html 里面,vscode 无法很好地识别模板语法,容易误报js语法错误。你不需要试图修复。
AI 生成的 app.py
长达 629 行,很难想象人类会这样写代码。我们来看一眼:

冗长的代码会带来很多后果。不利于人类理解,也不利于维护。且要处理的文本变长,会导致 LLM 表现变差、成本变高。因此,在使用 Cline 造了几个项目之后,笔者的结论是:除非整个项目都打算由 Cline 写,否则不要引入 Cline。
建议抛弃 langchain
Anthropic 在一篇指引中建议直接调用 LLM API,不依赖其他框架。笔者在编程实践之后,认为这话言之有理,我们确实应该抛弃 langchain。主要原因有二:
- langchain 的 API 变化频率,远非工业级软件的作风。一个已经被大众广泛接受的函数,数月之后就可能被弃用、被移动到其他包、或者因为第三方库的破坏性更新而停摆。尽管 AI 社区的很多软件都有这类恶习,但 langchain 显得尤其突出。
- langchain 代码质量不佳。以 RAG 为例,langchain 的 pgvector 适配器有如下缺陷:(一)使用 psycopg2 而不是 psycopg3;(二)它在 db 中建了
collection
表和embedding
表,其中collection
表的主键是 uuid 格式,embedding
表中的collection_id
字段也是 uuid 格式,但它自己的主键竟是 varchar 格式的 uuid,令人错愕。

笔者建议使用 OpenAI 的官方 sdk。原因在于:
- 大部分 LLM 提供商都兼容 OpenAI API 接口,这套 API 已经成为事实标准。
- 如果一个本来兼容 OpenAI sdk 的服务变得不再兼容,则马上会被发现并修复。
关于函数调用
强烈建议不要依赖 LLM 提供商的 function call 功能。这方面我们可以学习 Cline,在 prompt 中告知 LLM 可以使用哪些工具,并要求 LLM 输出一个包含参数的 xml。
参考 prompt:
你是一个在互联网巨头公司工作的软件工程师。
你可以查询数据库。这个数据库的建表语句如下:
<db-init>
...
</db-init>
你需要执行以下步骤:
1. 思考当前的局面,以及你接下来该如何处理问题,以 <think>[你的思考...]</think> 输出
2. 在以下行为中选择一个(且仅一个)执行:
2.1 如果你需要收集信息,则你提供一个 sql 语句,用户会执行之,并将结果告诉你。
2.2 如果你认为以现有信息足够回答用户的问题,则输出回答。
如果你选择收集信息,则把你希望执行的 sql 语句以下面的格式输出:
<sql>[你需要执行的语句...]</sql>
如果你选择回答问题,则如此输出:
<answer>[你的答案...]</answer>
你必须输出 <think> 标签。至于 <sql> 和 <answer> 标签,你要么输出 <sql> 标签,要么输出 <answer> 标签,不能两个都输出,也不能都不输出。
注意,你的输入处理能力是有限的,你必须保证“对话历史 + 接下来的 SQL 执行结果”总和不超过 30000 字节。
这里有一些提示,帮你更好地完成任务:
- 生成 sql 语句时,考虑面向索引优化
- RDBMS 是 postgresql 17
- 为了防止超出处理能力,如果要查询多行,则你必须先查询它有多少行
- <think>与其他标签之间,要以空行分割
- 如果从数据库中提取时间戳,则使用 TO_CHAR(..., 'YYYY-MM-DD HH24:MI:SS') 格式
- ...
LLM 选型
“为 workflow/agent 选择 LLM”与“为聊天选择 LLM”有本质区别。workflow 和 agent 的 prompt 一般比较复杂,我们需要 LLM 有较强的指令跟随能力;与此同时,我们希望调用成本尽可能低。这需要进行一系列测试,笔者建议考虑以下 LLM:
- gpt-4o、claude 3.7 sonnet、gemini 2.0 flash
- deepseek-v3、qwen2.5-max
- 豆包 1.5 pro(成本友好,适合用于洗数据)
- qwen-7b、qwen-32b、qwen-72b(开发期间调用硅基流动的服务,日后可以迁移到本地部署)