1. 从开源项目到实战指南:我的LLM应用架构探索之路
过去半年,我几乎把所有业余时间都泡在了大语言模型(LLM)的应用开发上。和Thoughtworks的同事、开源社区的伙伴们一起,我们折腾出了一系列项目,从Prompt工程到应用架构,从微调模型到IDE插件,几乎把LLM能落地的方向都摸了一遍。这个过程里,我最大的感触是:技术浪潮来了,光看热闹没用,得亲手去“造轮子”,才能真正理解它为什么转、怎么转、以及转起来会卡在哪儿。市面上关于ChatGPT、AIGC的讨论很多,但大多是概念和演示,缺的是从零到一构建一个可维护、可扩展、真正解决实际问题的LLM应用的那套“工程化”心法。这正是我和伙伴们通过“phodal/aigc”这一系列开源项目想要填补的空白——它不是一本教科书,而是一份来自一线实战的、带着温度与坑点的架构设计笔记。
如果你是一名开发者、技术负责人,或者对如何将LLM能力深度集成到自己的产品或研发流程中感兴趣,那么接下来我要分享的,正是我们趟过的路、总结的模式和踩过的坑。我们将围绕几个核心问题展开:如何系统性地设计和编写Prompt?如何为LLM设计友好的应用架构?如何基于开源模型定制专属能力?以及,LLM将如何重塑我们熟悉的软件开发工序?我会尽量避免空泛的理论,而是结合我们具体的开源项目,拆解背后的设计思路、技术选型和实操细节。无论你是想快速上手构建一个聊天应用,还是计划在团队内引入AI辅助编程,相信这些从真实项目中沉淀的经验,都能给你带来直接的参考。
2. LLM能力基石:超越简单问答的Prompt工程体系
很多人对LLM应用的第一印象,就是打开ChatGPT的网页对话框,输入一个问题,然后等待一个答案。这确实是最直接的交互,但如果你想构建一个稳定、可靠、能处理复杂逻辑的应用,这种“一次性问答”的模式是远远不够的。Prompt(提示词)是驱动LLM的“代码”,其质量直接决定了应用的智商上限。我们意识到,必须像管理代码一样去管理Prompt,并为其建立一套工程方法。
2.1 Prompt的编写模式:从技巧到可复用的设计模式
早期使用Prompt,大家热衷于收集各种“神奇咒语”,比如“请扮演一个资深专家…”。但这是一种脆弱的方式,换一个模型或场景可能就失效了。我们从软件设计模式中汲取灵感,开始系统性地归纳Prompt编写模式。这不仅仅是技巧,而是解决某一类问题的可复用方案。
例如,我们总结的“链式思考(Chain-of-Thought)”模式,核心不是简单要求模型“逐步思考”,而是通过精心设计的Prompt结构,引导模型显式地展示推理过程。在一个客服场景的意图分类项目中,我们最初的Prompt是:“判断用户query的意图类别:咨询、投诉、办理。”效果不稳定。后来我们应用了链式思考模式:
你是一个客服意图分类专家。请按以下步骤工作: 1. 首先,逐句分析用户query,提取关键实体和情绪词。 2. 然后,根据第一步的提取结果,对照意图定义进行匹配: - 咨询:包含“怎么”、“如何”、“多少钱”、“时间”等疑问词,寻求信息。 - 投诉:包含“不满”、“差”、“慢”、“故障”等负面词汇,表达抱怨。 - 办理:包含“申请”、“开通”、“预约”、“提交”等动作词,希望完成业务。 3. 最后,综合以上分析,输出最匹配的单一意图类别。 用户query:{用户输入}这个模式的成功在于,它将黑盒的思考过程“白盒化”,不仅提高了分类准确率,更重要的是,当分类出错时,我们可以回溯到模型的“思考步骤”进行调试,比如是实体提取错了,还是匹配规则理解有偏差。这为Prompt的迭代优化提供了清晰的路径。
另一个关键模式是“少样本学习(Few-Shot Learning)”模式。当任务复杂或定义模糊时,单纯用语言描述不如直接给例子。我们在开发一个从自然语言生成数据报表SQL语句的应用时,就大量使用了此模式。Prompt中会包含3-5个精心挑选的“用户问题-SQL语句”对,这些例子覆盖了不同的查询维度(如时间筛选、分组聚合、多表关联)。模型通过类比这些例子来生成新的SQL。这里的选择和构造示例本身就是一门学问,需要覆盖边界情况和常见错误,我们通常会结合历史查询日志来构建这个示例库。
实操心得:Prompt模式不是银弹盲目套用模式可能适得其反。我们曾在一个创意写作项目中机械地使用链式思考,导致生成的文案僵硬、缺乏灵气。后来意识到,对于发散性任务,更需要的是“角色扮演(Role-Playing)”模式或“种子词(Seed Word)”激发模式。关键是根据任务类型(分类、生成、推理、创意)选择合适的模式或组合模式。建立一个团队的Prompt模式库,并附上适用场景和效果说明,能极大提升协作效率。
2.2 Prompt即代码:版本化、测试与协作管理
当Prompt数量增多、逻辑变复杂后,像管理配置文件一样用文本文件存储会很快陷入混乱。我们提出了“Prompt即代码”的理念,并实践在ClickPrompt等工具中。这意味着:
- 版本控制:使用Git管理Prompt的迭代历史,清晰看到每次修改的内容、作者和意图,便于回滚和审计。
- 模块化与复用:将常用的Prompt片段(如系统指令、示例库、输出格式规范)抽象为可导入的模块。例如,一个“安全审查”模块可以被多个业务Prompt引入。
- 测试与验证:为关键Prompt编写测试用例。这可以是基于输入-输出的断言测试,也可以是使用另一组LLM调用进行质量评估的“模型测试”。我们在CI/CD流水线中集成Prompt测试,确保修改不会破坏现有功能。
- 参数化与模板:将Prompt中可变的部分(如用户输入、业务规则)参数化,通过模板引擎动态生成最终Prompt。这使得同一套逻辑能快速适配不同场景。
在我们的实践中,一个用于代码审查的Prompt可能会被组织成这样的结构:
prompts/ ├── system_instructions/ │ └── senior_engineer.md # 定义角色和基础原则 ├── templates/ │ └── code_review.j2 # Jinja2模板,包含参数占位符 ├── examples/ │ └── good_feedback.json # 少样本示例数据 └── tests/ └── test_review.py # 使用pytest,验证对典型代码片段的输出是否包含关键点通过这种方式,Prompt从散落的“魔法字符串”变成了可维护、可测试的工程资产。
3. LLM时代的应用架构设计:迎接以AI为核心的变化
当LLM从对话界面走向业务系统后端,传统的分层架构(如MVC)开始显得力不从心。LLM的不确定性、长上下文、流式响应等特性,要求我们重新思考应用架构。我们探索并提出了Unit Mesh架构思想,其核心是将LLM视为一个具有强大认知和生成能力的“计算单元”,围绕它构建松散耦合、事件驱动、可观测的服务网格。
3.1 交互范式演进:从ChatUI到AI-Agent工作流
ChatUI(聊天界面)是LLM最自然的交互形式,但在复杂业务中,单一的聊天窗口容易成为“垃圾输入、垃圾输出”的黑洞。我们通过ChatFlow项目探索了如何将聊天升级为结构化的工作流。
例如,一个旅游规划Agent,不再是用户问“帮我规划一下北京三日游”,然后模型直接吐出一大段文本。ChatFlow会将其分解为一个可执行的工作流:
- 意图识别与槽位填充:首先用一个专门的LLM调用或分类器,识别用户意图为“旅游规划”,并提取关键槽位:目的地=北京,天数=3,未明确槽位(如预算、兴趣)待澄清。
- 多轮澄清:自动追问用户:“您的预算是多少?对历史古迹、美食还是自然风光更感兴趣?”
- 子任务编排:根据填充完整的槽位,并行或串行调用多个“技能单元”(Unit):
- 信息查询Unit:调用搜索引擎API或知识库,获取北京景点、天气、交通信息。
- 行程生成Unit:由LLM基于查询结果,生成详细的每日行程草案。
- 预算估算Unit:根据行程,调用价格查询API估算大致费用。
- 结果整合与呈现:将各个Unit的结果汇总,由LLM润色成一份完整的、个性化的旅游计划,并以图文并茂的形式(甚至可生成地图草图)返回给用户。
在这个架构下,LLM扮演了“大脑”的角色,负责理解、规划和协调,而具体的、确定性的任务(如数据查询、计算)则由传统的、可靠的微服务(Unit)完成。这种模式显著提升了复杂任务的完成度和可靠性。
3.2 Unit Mesh架构详解:弹性、可观测与治理
Unit Mesh架构的灵感来源于服务网格(Service Mesh),但将“服务”替换为功能各异的“Unit”(单元)。一个Unit可以是一个纯LLM调用,一个传统API,一个数据库查询,或者它们的组合。
核心组件包括:
- AI Unit:封装了LLM调用,包含Prompt模板、模型配置、上下文管理逻辑。它是处理非确定性任务的核心。
- Tool Unit:封装了确定性的工具调用,如计算器、搜索引擎、业务API。
- Orchestrator(编排器):负责接收用户请求,理解意图,并动态编排AI Unit和Tool Unit的执行流程。它本身可以是一个LLM(基于ReAct等框架),也可以是一个基于规则或图的引擎。
- Context Broker(上下文经纪人):管理对话或工作流的上下文状态,确保信息在不同Unit间正确传递。这是解决LLM“记忆力”有限的关键。
- Observability Layer(可观测层):必须全面监控每个LLM调用的耗时、Token消耗、费用、输入输出,并进行日志记录和追踪。这对于调试、成本控制和效果优化至关重要。
我们在Unit Runtime项目中提供了一个轻量级的运行时环境,帮助开发者快速搭建和测试这样的AI应用流水线。它允许你以配置的方式定义Units和流程,并一键启动一个可交互的测试环境。
避坑指南:LLM应用架构的三大陷阱
- 过度依赖LLM:试图用LLM处理所有逻辑,包括精确计算、实时数据获取,导致响应慢、成本高、结果不稳定。正确做法:严格区分“认知型任务”(适合LLM)和“执行型任务”(适合传统代码),遵循“LLM as a Coordinator”原则。
- 上下文管理混乱:简单地将所有历史对话都塞进上下文,很快会触及Token限制,且无关信息会干扰模型。正确做法:实现智能的上下文窗口管理,如摘要历史、选择性记忆、向量检索关联片段。
- 缺乏可观测性:把LLM当黑盒,出了问题无法定位是Prompt问题、模型问题还是数据问题。正确做法:在架构设计初期就植入可观测性,记录每次调用的Prompt、参数、完整响应和中间结果,建立效果评估指标(如准确率、用户满意度)。
4. 面向特定场景的深度定制:从Prompt工程到模型微调
对于通用问题,ChatGPT等大模型已经足够出色。但一旦涉及专业领域(如医疗、法律、金融)或企业内部私有知识,通用模型就会显得“泛而不精”。这时就需要走向深度定制,我们探索了两条主要路径:上下文工程(Prompt Engineering)和模型微调(Fine-Tuning)。
4.1 上下文工程:让模型“阅读”你的知识库
这是成本较低、见效较快的方式。核心思想不是改变模型本身,而是通过Prompt为模型提供相关的上下文信息。检索增强生成(RAG)是当前最主流的实现模式。
其工作流程如下:
- 知识库预处理:将你的文档(PDF、Word、Wiki等)进行切片,转换成文本片段。
- 向量化与索引:使用嵌入模型(如OpenAI的text-embedding-ada-002)将文本片段转换为向量,存入向量数据库(如Pinecone、Chroma、Milvus)。
- 检索:当用户提问时,将问题也转换为向量,在向量数据库中搜索最相似的几个文本片段。
- 增强生成:将检索到的相关片段作为上下文,和用户问题一起构造最终的Prompt,送给LLM生成答案。
我们实践中的关键点在于检索质量。如果检索到的片段不相关,LLM就会“胡编乱造”。我们通过以下方式优化:
- 智能分块:不是简单按字数切分,而是根据文档结构(如标题、段落)进行语义分块,避免将一个完整概念切断。
- 多路召回:结合关键词搜索(如BM25)和向量搜索,兼顾精确匹配和语义相似度。
- 重排序(Re-ranking):使用一个更精细的交叉编码器模型对初步检索的结果进行重新排序,将最相关的排在前面。
在ArchGuard Co-mate项目中,我们应用RAG构建了一个架构知识助手。它将架构决策记录、设计模式文档、系统架构图描述等存入向量库。当开发者询问“我们系统目前为何采用事件驱动架构?”时,RAG会检索到相关的决策文档和会议纪要作为上下文,让LLM生成一个基于事实的、准确的回答,而不是凭空想象。
4.2 模型微调:打造专属的“领域专家”
当你的任务非常特定、且拥有大量高质量数据时,微调是更彻底的选择。它能让模型学习到特定的风格、格式和领域知识。我们通过Unit Minions项目深入实践了基于开源模型(如LLaMA、ChatGLM)的微调。
微调的核心步骤:
- 数据准备:这是最耗时但最关键的一步。你需要准备大量“指令-输出”对。例如,对于代码生成任务,数据格式可能是
{"instruction": "用Python写一个快速排序函数", "output": "def quicksort(arr): ..."}。数据质量要求极高,需要清洗、去重、格式化。 - 选择基座模型与微调方法:
- 全参数微调:效果最好,但需要大量GPU资源,适用于数据量极大、不差钱的场景。
- 参数高效微调(PEFT):如LoRA(Low-Rank Adaptation),这是我们主要采用的方法。它只训练模型参数中插入的一些低秩矩阵,大大减少了计算量和显存消耗,效果接近全参数微调。在Unit Minions中,我们提供了详细的LoRA训练脚本和配置。
- 训练与评估:在准备好的数据上运行训练脚本。训练过程中和结束后,需要在独立的验证集上评估模型效果,评估指标可能包括生成代码的编译通过率、测试用例通过率、文本生成的BLEU分数等。
- 部署与应用:将训练好的LoRA权重与基座模型合并,导出为可服务的模型文件,通过类似Text Generation Inference(TGI)或vLLM这样的高性能框架进行部署。
我们在DevTi项目中微调了一个专注于“从用户故事生成测试用例”的模型。我们收集了数千条高质量的用户故事及其对应的Gherkin语法测试场景作为训练数据。微调后的模型,在生成测试用例的完整性、准确性和格式规范性上,远超通用模型。
微调实战经验:数据与评估是关键
- 数据质量 > 数据数量:1000条精心构造、标注一致的数据,远胜于10万条嘈杂、矛盾的数据。我们花了70%的时间在数据清洗和标注上。
- 警惕过拟合:如果模型在训练集上表现完美,在验证集上却很差,那就是过拟合。需要增加数据多样性、使用数据增强、或调整正则化参数。
- 评估要贴近真实场景:不要只看损失函数下降。设计端到端的评估任务,比如让微调后的模型直接生成一段代码,然后跑单元测试看通过率。我们建立了自动化的评估流水线,每次训练后自动运行。
- 成本考量:即使是LoRA,训练也需要GPU资源。云服务按小时计费,需要精确预估时间和成本。对于大多数中小企业,从高质量的Prompt工程和RAG开始,性价比更高。
5. 重塑软件开发工序:AI原生研发流程的实践
LLM和Copilot类工具的出现,不仅仅是多了个“自动补全”,它正在改变需求分析、设计、编码、测试、运维的每一个环节。我们思考并实践了一套AI 2.0时代的软件开发工序。
5.1 需求与设计阶段:从模糊想法到清晰规约
传统上,产品经理撰写冗长的PRD,工程师再将其转化为技术方案,存在信息损耗。现在,LLM可以成为中间的“翻译器”和“加速器”。
- 自动化用户故事拆分:我们将粗略的产品需求描述输入给像DevTi这样的微调模型,它可以自动生成格式规范的用户故事(As a... I want... So that...)和验收标准。
- 架构探索与决策辅助:在ArchGuard Co-mate中,我们可以描述系统概要和约束(如“高并发、数据最终一致性”),让LLM基于内置的架构知识库,生成几个备选的架构模式(如CQRS、事件溯源),并分析其利弊,辅助架构师决策。
- 生成设计草案:根据用户故事,LLM可以快速生成数据库ER图的草稿、API接口的粗略定义,甚至UI线框图描述,极大提升了早期设计和沟通的效率。
5.2 编码与测试阶段:从助手到协同者
这是GitHub Copilot等工具已经大显身手的领域,但我们的AutoDev插件试图走得更远。它不仅仅是代码补全,而是深度集成到IDE和研发流程中的“AI副驾驶”。
- 上下文感知的代码生成:AutoDev能读取你当前的代码文件、项目结构、甚至关联的Jira任务描述,生成高度相关、符合项目规范的代码。例如,当你在实现一个“用户注册”API时,它知道你已经在
User实体中定义了哪些字段,并据此生成相应的DTO、Service层方法和Controller。 - 自动化测试生成:基于已有的业务代码,自动生成单元测试框架。它不仅能生成测试用例,还能尝试理解业务逻辑,生成有意义的测试数据和对边界情况的测试。
- 智能代码审查:结合团队定义的编码规范和安全规则(如SQL注入、硬编码密码),AutoDev可以在代码提交前进行实时审查,提出改进建议,而不仅仅是检查语法。
- 自动化代码重构:接受如“将这个函数拆分成两个,提高可读性”这样的自然语言指令,并安全地执行重构操作。
5.3 运维与知识管理:构建自进化的系统
LLM的应用不止于开发期。
- 智能日志分析:传统的日志检索依赖关键词,难以发现复杂关联。通过LLM分析实时日志流,可以自动归纳异常模式、推测根因,并用自然语言生成运维报告。
- 自动化文档生成与更新:代码变更后,LLM可以自动分析diff,更新对应的API文档、部署手册或内部Wiki。我们尝试让CI流水线在每次合并后,自动调用LLM生成本次更新的变更摘要。
- 构建团队知识大脑:将项目文档、会议纪要、故障报告、优秀代码案例全部通过RAG注入到一个内部知识助手。新成员可以随时提问,快速了解项目历史和设计决策;老成员也能快速检索到模糊记忆中的细节。
这一套工序的改变,其核心是将人类从重复性、机械性的脑力劳动中解放出来,更专注于高层次的创意、决策和复杂问题求解。人机协同,而非机器替代,是现阶段更现实的图景。
6. 常见问题与实战排查指南
在实际开发和部署LLM应用的过程中,我们遇到了无数的问题。下面将一些最常见的问题及其排查思路整理成表,希望能帮你少走弯路。
| 问题现象 | 可能原因 | 排查步骤与解决方案 |
|---|---|---|
| LLM响应内容完全无关或胡言乱语 | 1. Prompt指令不清晰或矛盾。 2. 上下文窗口被无关历史淹没。 3. 模型温度(temperature)参数过高。 | 1.简化并强化指令:在Prompt开头用“你是一个…,你必须…”明确角色和任务。使用分隔符(如```)清晰区分指令和内容。 2.清理上下文:实现上下文管理策略,只保留最近几轮或最相关的对话。对于长对话,尝试让模型自行总结之前的关键点。 3.调整参数:将 temperature调低(如0.1-0.3)以获得更确定性的输出。 |
| 处理长文档或复杂任务时,模型“忘记”了前文指令 | 超出了模型的上下文长度限制。 | 1.分而治之:将长文档分割成块,分别处理后再汇总。使用Map-Reduce模式:让LLM先对每个块生成摘要(Map),再对所有摘要进行总结(Reduce)。 2.外部记忆:使用向量数据库(RAG)存储文档知识,每次只检索相关片段送入上下文。 3.升级模型:考虑使用支持更长上下文(如128K)的模型。 |
| 基于RAG的应用,答案不准确或包含幻觉 | 1. 检索到的文档片段不相关。 2. 即使片段相关,模型也未正确利用。 | 1.优化检索:检查嵌入模型是否适合你的领域;尝试混合检索(关键词+向量);调整检索返回的数量(k值);引入重排序模型。 2.优化Prompt:在Prompt中明确指示模型“严格基于以下上下文回答,如果上下文未提供信息,请直接说不知道”。在上下文中用明显标记(如 ##参考文档##)。3.添加引用溯源:要求模型在答案中注明依据的原文片段编号,便于人工复核。 |
| 微调后的模型效果不如预期 | 1. 训练数据质量差或数量不足。 2. 过拟合或欠拟合。 3. 任务定义或评估方式有问题。 | 1.审查数据:随机抽样检查训练数据,确保“指令-输出”对高质量、无噪声。尝试增加数据量或进行数据增强。 2.分析学习曲线:观察训练集和验证集损失。如果验证集损失先降后升,是过拟合,需增加正则化、减少模型容量或增加数据。如果两者都高,是欠拟合,可能需增加模型容量或训练轮数。 3.端到端测试:用一批未参与训练的真实任务测试模型,进行人工评估,这比单纯的指标更可靠。 |
| 应用响应速度慢,延迟高 | 1. LLM API调用网络延迟高。 2. 模型自身生成速度慢(特别是大模型)。 3. 应用逻辑复杂,串行调用过多。 | 1.选择合适的地理区域:如果使用云服务,确保API端点离你的用户或服务器最近。 2.模型选型:在效果和速度间权衡。对于实时性要求高的场景,考虑更小的模型或推理优化框架(如vLLM)。使用流式响应(streaming)让用户先看到部分结果。 3.异步与并行:将独立的LLM调用或工具调用改为异步并行。优化工作流,减少不必要的步骤。 |
| Token消耗巨大,成本失控 | 1. Prompt过于冗长,包含不必要信息。 2. 上下文管理不当,积累了太多历史。 3. 未对输入输出长度进行限制。 | 1.精简Prompt:删除冗余的描述和示例。使用更高效的指令。 2.压缩上下文:对历史对话进行智能摘要。在RAG中,优化检索策略,只送入最相关的少量片段。 3.设置硬性限制:在代码中为输入和输出设置max_tokens限制,并做好截断和异常处理。 4.监控与告警:建立成本监控仪表盘,设置每日/每周Token消耗告警阈值。 |
7. 开源项目实战:从想法到可运行的原型
理论再多,不如亲手运行一个例子。让我以构建一个简单的“智能技术文档助手”为例,串联起前面提到的多项技术。这个助手能基于你的项目源码和文档,回答技术问题。
7.1 技术栈选择与项目初始化
我们选择轻量、易上手的方案:
- 后端框架:FastAPI(Python),轻量且异步支持好。
- LLM接口:OpenAI API(或兼容的开源模型本地部署,如通过Ollama运行Llama 3)。
- 向量数据库:ChromaDB,轻量级,易于集成,适合原型开发。
- 文本嵌入模型:all-MiniLM-L6-v2(Hugging Face),一个轻量且效果不错的开源句子嵌入模型,可本地运行,避免API调用。
- 前端:简单的Streamlit界面,快速构建交互。
首先创建项目并安装依赖:
mkdir tech-doc-assistant && cd tech-doc-assistant python -m venv venv source venv/bin/activate # Windows: venv\Scripts\activate pip install fastapi uvicorn chromadb sentence-transformers streamlit openai pypdf27.2 核心模块一:文档处理与向量化
创建ingest.py,负责读取文档(假设为PDF和Markdown),分块并存入向量库。
import os from PyPDF2 import PdfReader from sentence_transformers import SentenceTransformer import chromadb from chromadb.config import Settings # 初始化嵌入模型和向量数据库客户端 embed_model = SentenceTransformer('all-MiniLM-L6-v2') chroma_client = chromadb.Client(Settings(chroma_db_impl="duckdb+parquet", persist_directory="./chroma_db")) collection = chroma_client.create_collection(name="tech_docs") def extract_text_from_pdf(pdf_path): """从PDF提取文本""" reader = PdfReader(pdf_path) text = "" for page in reader.pages: text += page.extract_text() + "\n" return text def split_text(text, chunk_size=500, chunk_overlap=50): """按语义和长度分块,这里简化处理按固定长度""" words = text.split() chunks = [] for i in range(0, len(words), chunk_size - chunk_overlap): chunk = " ".join(words[i:i + chunk_size]) chunks.append(chunk) return chunks def ingest_documents(docs_dir): """遍历目录,处理所有文档""" doc_id = 0 for filename in os.listdir(docs_dir): if filename.endswith(".pdf"): path = os.path.join(docs_dir, filename) text = extract_text_from_pdf(path) chunks = split_text(text) for chunk in chunks: # 生成向量 embedding = embed_model.encode(chunk).tolist() # 存入ChromaDB collection.add( documents=[chunk], embeddings=[embedding], metadatas=[{"source": filename}], ids=[f"doc_{doc_id}"] ) doc_id += 1 # 类似地处理.md文件... print(f"已入库 {doc_id} 个文本块。") if __name__ == "__main__": ingest_documents("./your_docs_directory")注意:实际生产环境中,分块策略需要更精细,比如按标题、段落进行语义分块,并使用更好的PDF解析库(如
pymupdf)。同时,需要处理文档更新和去重。
7.3 核心模块二:检索与问答接口
创建api.py,提供检索和问答的FastAPI端点。
from fastapi import FastAPI, HTTPException from pydantic import BaseModel from sentence_transformers import SentenceTransformer import chromadb from chromadb.config import Settings import openai import os app = FastAPI() embed_model = SentenceTransformer('all-MiniLM-L6-v2') chroma_client = chromadb.Client(Settings(persist_directory="./chroma_db", chroma_db_impl="duckdb+parquet")) collection = chroma_client.get_collection(name="tech_docs") openai.api_key = os.getenv("OPENAI_API_KEY") class QueryRequest(BaseModel): question: str top_k: int = 3 @app.post("/ask") async def ask_question(req: QueryRequest): # 1. 将问题转换为向量 query_embedding = embed_model.encode(req.question).tolist() # 2. 从向量库检索相关文档片段 results = collection.query( query_embeddings=[query_embedding], n_results=req.top_k ) if not results['documents']: raise HTTPException(status_code=404, detail="未找到相关文档。") # 3. 构建Prompt上下文 context = "\n\n---\n\n".join(results['documents'][0]) prompt = f"""你是一个技术文档助手,请严格根据以下提供的上下文信息来回答问题。如果上下文没有提供足够信息,请直接说“根据现有文档,我无法回答这个问题”。 上下文: {context} 问题:{req.question} 答案:""" # 4. 调用LLM生成答案 try: response = openai.ChatCompletion.create( model="gpt-3.5-turbo", # 或 "gpt-4" messages=[{"role": "user", "content": prompt}], temperature=0.1, max_tokens=500 ) answer = response.choices[0].message.content.strip() except Exception as e: raise HTTPException(status_code=500, detail=f"LLM调用失败: {str(e)}") # 5. 返回答案和引用来源 sources = [metadata.get("source", "未知") for metadata in results['metadatas'][0]] return { "answer": answer, "sources": list(set(sources)) # 去重 }7.4 前端界面与整合
创建app.py,使用Streamlit构建一个简单的Web界面。
import streamlit as st import requests import os st.title("📚 智能技术文档助手") st.markdown("基于项目文档,回答你的技术问题。") # 配置后端API地址 API_URL = os.getenv("API_URL", "http://localhost:8000") question = st.text_input("请输入你的问题:", placeholder="例如:如何配置数据库连接池?") if st.button("提问") and question: with st.spinner("正在检索文档并思考中..."): try: response = requests.post( f"{API_URL}/ask", json={"question": question, "top_k": 3} ) if response.status_code == 200: result = response.json() st.success("答案:") st.write(result["answer"]) if result["sources"]: st.info(f"参考文档:{', '.join(result['sources'])}") else: st.error(f"请求失败:{response.text}") except requests.exceptions.ConnectionError: st.error("无法连接到后端服务,请确保API服务已启动。")7.5 运行与测试
- 将你的技术文档(PDF/MD)放入
./your_docs_directory目录。 - 运行文档处理脚本:
python ingest.py。 - 启动后端API服务:
uvicorn api:app --reload --port 8000。 - 在另一个终端,启动前端界面:
streamlit run app.py。 - 打开浏览器访问Streamlit提供的地址(通常是
http://localhost:8501),即可开始问答。
这个原型虽然简单,但完整实现了RAG的核心流程。你可以在此基础上扩展:增加更多文件格式支持、优化分块和检索策略、加入对话历史管理、对接更强大的开源模型等。通过这个动手过程,你会对LLM应用开发的各个环节有更具体、更深刻的理解。