
2025年聊大模型应用开发已经没人会拿“能聊天”当卖点了。现在最常被问到的、也是落地价值最直接的两个方向就是RAG和Agent。RAG解决“模型不知道公司内部资料、一回答就编”的问题Agent解决“模型只会说、不能帮你干活”的问题。而LangChain这个生态最全、资料最多的编排框架把这两件事串了起来——从提示词、文档切分、向量检索到工具调用、状态流转都有对应的模块。这篇内容就是基于“从提示词到实战项目”这条学习主线把RAG与Agent智能体项目的完整脉络拆开讲清楚。我会从提示词工程开始逐步带你跑通知识库问答再进阶到Agent工具调用最后落到LangGraph FastAPI PGVector这套工程化方案上。适合正在从后端转AI应用开发的工程师、做企业内部知识库的数据团队以及那些已经把LangChain demo跑起来但不知道下一步怎么走的人。1. 项目全景与学习路线从提示词到RAG再到Agent1.1 为什么大模型应用开发绕不开LangChain先说一个真实感受大模型本身的能力边界越来越明显不管你用的是闭源API还是本地部署的开源模型单纯丢给它一个问题让它回答效果上限就是模型预训练时的知识边界。一旦涉及企业内部文档、实时数据、业务系统操作就必须在外面套一层“编排层”让模型知道该调什么工具、该看哪些资料、该怎么组织回答。LangChain做的就是这件事。它本质上不是一个模型而是一套把模型、向量库、外部工具、记忆模块串起来的标准化框架。你不需要自己去封装各家API的差异也不需要手工维护“检索-拼上下文-调模型-解析输出”这条流水线LangChain里都有现成组件。但这几年LangChain的生态变化也非常快。早期是Chain的概念一条链子从头走到尾后来引入了LCEL表达式语法支持复杂的组合再后来Agent火起来LangChain推出了LangGraph把控制流升级成了状态图。很多人会问LangChain和LangGraph到底有什么区别我自己的理解很简单——LangChain是“零件库”和“组装工具”LangGraph是“流水线调度器”。零件库负责提供模型封装、提示模板、向量库适配、工具定义这些基础能力调度器负责把多个节点串成有状态、有条件分支的完整流程。现在的Agent项目基本是两者配合使用。1.2 学习路线为什么要按“提示词→RAG→Agent”分层推进我见过不少新人一上来就追Agent结果卡在工具调用死活调不通。回头一看连基础提示词都没写好。我的建议是严格按四层递进层级能力方向典型产物关键工具第一层提示词工程稳定、可控的对话输出Prompt模板、Few-shot、思维链第二层RAG知识库基于私有文档的问答系统文档加载器、切分器、Embedding、向量库第三层Agent智能体能调用工具完成任务的流程工具定义、ReAct循环、记忆管理第四层工程化落地稳定上线的服务FastAPI、LangGraph、观测日志、评测集每一层都是下一层的地基。提示词写不好RAG检索回来再好的资料模型也可能答偏RAG链路不稳定Agent调检索工具时拿到的就是垃圾输入后续规划全乱。所以这篇内容我沿用这个路径把每一层的核心知识点和实操代码都过一遍。2. 提示词工程与RAG核心原理先看懂再动手2.1 提示词设计不是“会聊天”而是结构化约束网上天天有人讨论“提示词泄露”其实那些被扒出来的提示词本质上就是几段写得还算规范的角色设定和输出要求。真正决定系统上限的不是提示词里有多少花哨的修辞而是你有没有把业务约束、输入格式、输出格式、兜底策略写清楚。我通常把生产级提示词拆成五个部分角色定义、任务描述、输入变量、输出格式、异常兜底。比如一个知识库问答的System Prompt我常用的写法是这样你是一个企业知识库问答助手。你的任务基于给定的「参考材料」回答用户问题。 要求 1. 只能依据「参考材料」作答不得使用材料之外的先验知识 2. 如果参考材料中没有相关内容直接回答“未在知识库中找到相关信息” 3. 回答时先给出结论再列出理由最后用「引用来源」标注材料编号 4. 保持客观中立不编造数据不推测未提及的信息。 参考材料 {context} 用户问题 {question}这个模板看起来简单但它同时做了三件事限制了范围只能依据材料、兜住了幻觉没有就直说不没有、规定了格式结论-理由-引用。我在排查知识库回答质量问题时第一步永远先看Prompt第二步才看检索结果。很多时候问题根本不是检索不到而是模型“自由发挥”了。2.2 RAG四阶段索引、检索、增强、生成RAG全称是Retrieval-Augmented Generation检索增强生成。它的核心思路很好理解模型不知道答案那就先把相关资料“喂”到上下文里再让它基于这些资料作答。整个过程拆成四个阶段第一阶段是索引。把PDF、Word、网页这些原始文档加载进来清洗掉无效内容按一定规则切成小块再用Embedding模型把每块文本转成向量存入向量数据库。第二阶段是检索。收到用户问题后把问题也转成向量在向量库里计算相似度取出最相关的Top-K段文本。第三阶段是增强。把检索到的文本片段和用户问题重新拼接成Prompt塞给大模型。第四阶段是生成。模型基于增强后的上下文生成最终答案。这里有几个参数对效果影响极大几乎决定了系统的上限参数常见范围调整思路chunk_size300-800字中文太小则语义不完整太大则噪音多、检索精度下降overlap50-100字防止关键句被切在边界处丢掉top_k3-5太少可能漏信息太多会稀释注意力score阈值0.3-0.5视嵌入模型而定低于阈值强行回答最容易产生幻觉embedding模型bge-m3 / text-embedding-3-small中文业务强烈建议先试bge系列关于切分我再多说一句。文档切分不是拿着字符数硬切而是要尊重语义边界。比如一个表格被从中间切开检索到的片段就是残缺的一个完整的条款被切到两块里模型拼上下文时也容易拼错。业界的通行做法是先按标题结构和段落边界做层级切分再配合overlap做缓冲。这块做得细不细直接决定后面检索的命中率。3. LangChain关键模块拆解与代码级实操3.1 核心模块地图六个必须知道的组成部分LangChain 0.1之后做了大量模块拆分早期一个大包走天下的时代已经没了。现在接触一个新项目最先要分清楚这几个包langchain是核心API和链式组合语法langchain-openai负责OpenAI兼容接口的模型封装langchain-community提供了大量社区贡献的加载器、向量库适配langchain-core是所有基础类型的定义处比如Prompt模板、消息、输出解析器。从功能视角看六个模块必须理解清楚Models模型封装包括对话模型、文本嵌入模型统一了各家API的调用差异。Prompts提示词模板支持模板变量、消息列表管理、Few-shot示例注入。Chains流程编排把Prompt、模型、输出解析器按顺序串成一条可复用的链。Indexes / Retrievers文档加载、切分、存储、检索的完整工具链。Memory记忆模块支持对话历史、摘要记忆、向量记忆等。Agents智能体框架让模型能够根据任务自主选择并调用工具。对于RAG项目重点在前面四个模块对于Agent项目核心是最后一个加上工具定义。但所有模块都建立在同一个组合语法之上|符号前一个组件的输出作为后一个组件的输入。3.2 从零跑通一个最小的LCEL调用链安装依赖我建议用一个干净的虚拟环境避免和已有项目冲突。装这几个包就够了pip install langchain langchain-openai langchain-community langchain-core faiss-cpu pypdf然后写一个最小的调用链。注意这里我用了base_url参数指向一个OpenAI兼容的服务地址如果你用的是云厂商的国内大模型API或者本地用vLLM部署的开源模型只需要改这个地址和api_key其它代码完全不用动from langchain_openai import ChatOpenAI from langchain_core.prompts import ChatPromptTemplate from langchain_core.output_parsers import StrOutputParser llm ChatOpenAI( modelgpt-4o-mini, base_urlhttp://localhost:8000/v1, api_keyEMPTY, temperature0.2 ) prompt ChatPromptTemplate.from_messages([ (system, 你是一个严谨的中文助手回答要求简洁、准确。), (human, {question}) ]) chain prompt | llm | StrOutputParser() result chain.invoke({question: LangChain是什么}) print(result)这段代码做了三件事定义了模型、定义了提示模板、通过|把它们组合成一条链。对于刚接触LangChain的人来说先跑通这个最小链路再往里加RAG、加工具认知负担会小得多。3.3 跑通一条最小RAG链路接下来在最小链路的基础上加入检索。用PDF文档做例子流程是加载、切分、向量化、检索、生成五步from langchain_community.document_loaders import PyPDFLoader from langchain.text_splitter import RecursiveCharacterTextSplitter from langchain_openai import OpenAIEmbeddings from langchain_community.vectorstores import FAISS # 1. 加载PDF loader PyPDFLoader(company_policy.pdf) documents loader.load() # 2. 切分 splitter RecursiveCharacterTextSplitter( chunk_size500, chunk_overlap50, separators[\n\n, \n, 。, , , , , , ] ) chunks splitter.split_documents(documents) # 3. 向量化并存入FAISS embeddings OpenAIEmbeddings( modelbge-m3, base_urlhttp://localhost:8000/v1, api_keyEMPTY ) vectorstore FAISS.from_documents(chunks, embeddings) # 4. 创建检索器 retriever vectorstore.as_retriever(search_kwargs{k: 4})这里我特别解释一下RecursiveCharacterTextSplitter里的separators参数。它的切分逻辑是递归的先尝试按段落分隔符\n\n切如果切出来的块还大于chunk_size就往下尝试按句号切再按逗号切。这个策略的好处是优先保证语义完整而不是机械地按字符长度砍。中文场景里。这些分隔符必须手动加进去默认的英文分隔符对中文的支持并不好这也是很多人切出来一堆乱块的原因。检索器和Prompt拼接好之后生成环节就清晰了from langchain_core.runnables import RunnablePassthrough def format_docs(docs): return \n\n.join(f[{i1}] {doc.page_content} for i, doc in enumerate(docs)) rag_chain ( {context: retriever | format_docs, question: RunnablePassthrough()} | prompt | llm | StrOutputParser() ) answer rag_chain.invoke(员工的年假标准是什么) print(answer)到这里一条可用的RAG链路就跑通了。你可以在本地任意放几篇制度文档试试效果后面所有优化都是在这个基础骨架上做文章。4. 实战一基于RAG的企业知识库问答系统4.1 需求拆解与技术选型先给一个真实场景一家几百人的公司制度文档散落在十几个PDF、几十条钉钉消息和若干Word文件里。HR每天都在群里回复“年假怎么算”“报销上限多少”这类重复问题。这个系统要做的就是把文档统一入库让员工用自然语言提问系统直接给出带出处的答案。技术选型上优先考虑团队维护成本和数据量级。我现在做项目时会这样区分场景方案原因快速验证demo、个人项目FAISS零部署成本本地文件即可中小团队生产环境PGVectorPostgreSQL自带扩展有权限控制和事务能力复用已有DB大规模检索、高并发Milvus / Qdrant分布式扩展能力强适合千万级向量文档量在10万片以内、并发也不高的场景直接上PGVector基本是性价比最高的选择。它依托PostgreSQL企业安全审计、数据备份、权限管理这些都不用自己重新造轮子。FAISS更适合开发自测。4.2 完整落地步骤从文档清洗到API接口企业文档入库前必须做清洗这一步被很多人忽略。我处理制度文档的经验是先把PDF转成文本用正则去掉页眉页脚页码遇到表格要单独提取并转成Markdown或键值对形式直接转纯文本会把对应关系拆散最后人工抽查几页确认切分后的每个片段都是能独立阅读的。清洗和切分完成向量化入库检索器建好剩下就是用FastAPI把能力暴露成HTTP接口。我建议返回结构里除了answer一定要带上source_documents或references这不仅是产品交互层面的需求也是排查线上badcase的依据from fastapi import FastAPI from pydantic import BaseModel app FastAPI() class QueryRequest(BaseModel): question: str top_k: int 4 app.post(/query) def query(req: QueryRequest): docs retriever.invoke(req.question) context format_docs_with_index(docs) answer rag_chain.invoke({context: context, question: req.question}) return { answer: answer, references: [ {index: i 1, content: doc.page_content, source: doc.metadata.get(source, )} for i, doc in enumerate(docs[: req.top_k]) ] }真实项目里context拼装、format_docs、API返回结构都要在一开始设计好。系统上线后你会发现日志里保存的“原始问题-检索片段-最终答案”三元组是所有优化工作的第一手资料。没有这段记录的RAG系统出了问题只能在黑盒里瞎猜。4.3 效果调优三板斧检索、提示词、上下文知识库上线后一定会遇到两类典型问题找不到和乱回答。我的排查顺序永远固定。第一板斧查检索。如果相关片段压根没被召回问题大概率出在切分粒度、Embedding模型和top_k参数上。把日志里的检索片段拉出来看如果召回的片段和问题明显不相关先去调score阈值再看切分粒度是否需要调整。中文场景下我用bge-m3的命中率明显优于通用英文Embedding模型而且它对长文本、多文档的支持更稳。第二板斧查提示词。如果检索片段已经包含答案但模型还是答错那就是Prompt约束不够。要么是没写死“只能依据参考材料回答”要么是输出格式设计得太宽松模型自作主张补充了材料之外的信息。这里没有捷径逐字逐句地收紧约束。第三板斧查上下文。有些问题分散在多个片段里比如“年假和调休可以叠加吗”材料A讲年假规则材料B讲调休规则单靠top_k取前4段可能只覆盖一边。解决思路是做两层检索先粗召回10段再做重排序Rerank或者把检索策略改成按问题类型先定位文档再在文档内部做细粒度检索。关于评测我不建议凭感觉调优。从历史客服记录或HR高频问题里整理30到50个问题人工写好标准答案和对应材料编号做成固定评测集。每次改动之后跑一遍对比答案正确率和引用正确率再决定是否上线。没有评测集的知识库调优跟闭着眼开车没区别。5. 实战二构建可稳定运行的Agent智能体项目5.1 Agent的本质从“回答问题”到“完成任务”如果说RAG给大模型装上了“参考书”Agent就是给大模型装上了“手脚”。它的核心不再是一次性回答而是根据目标自主规划步骤、调用工具、观察结果、继续执行直到任务完成。我经常用实习生来类比你给实习生一个目标“查一下上个月各区域的订单量并把报表发给我”他不会直接背答案而是会去数据库里执行查询、把结果整理成表格、再发给你。Agent做的事情就是这个只是把“去数据库查”“整理表格”“发邮件”这些动作变成了模型可调用的工具函数。一个完整的Agent由四个部分组成模型负责推理决策规划负责拆解任务记忆负责保存对话历史和中间结果工具负责与外部世界交互。工具是Agent和RAG最大的分水岭——RAG的输入输出都是纯文本Agent的输入输出是“函数调用”。这里也要提醒一句不是所有场景都适合上Agent。如果你的业务流程是固定的三步走用Chain硬编码性价比最高稳定还便宜。Agent的优势在不确定性比如用户诉求多变、解决路径需要动态规划的场景。流程完全确定的系统强行套Agent除了增加成本和故障点没有任何收益。5.2 用LangChain实现一个带工具的Agent在LangChain中定义工具非常直接。拿一个查订单状态的工具举例from langchain_core.tools import tool tool def get_order_status(order_id: str) - str: 根据订单ID查询订单当前状态返回状态、物流信息和预计送达时间。 # 实际项目里这里调用内部订单系统的API status_map { A1001: 已发货物流单号SF1234567890, A1002: 待支付, } return status_map.get(order_id, 订单不存在)注意工具函数的三个关键点缺一不可。一是tool装饰器它把普通函数转成LangChain工具二是函数的docstring模型就是通过它来理解“这个工具是干什么用的”三是类型注解和参数说明模型需要知道该传什么样的参数。工具定义得越清楚调用成功率越高。有了工具之后创建Agent。我常用create_tool_calling_agent它需要三个东西模型、工具列表、提示模板from langchain.agents import create_tool_calling_agent, AgentExecutor tools [get_order_status] agent_prompt ChatPromptTemplate.from_messages([ (system, 你是一个订单查询助手。用户提供订单信息时请调用工具查询真实状态不要猜测。), (human, {input}), (placeholder, {agent_scratchpad}), ]) agent create_tool_calling_agent(llm, tools, agent_prompt) agent_executor AgentExecutor( agentagent, toolstools, max_iterations5, verboseTrue ) response agent_executor.invoke({input: 帮我查一下订单A1001到哪了}) print(response[output])这里agent_scratchpad是LangChain内部记录中间推理过程的占位符核心作用是让模型能看到之前已经执行过哪些工具调用、得到什么结果避免重复执行或逻辑断裂。max_iterations5是一道保险丝防止模型陷入死循环烧掉大量token。5.3 Agent项目稳定运行的工程要点把Agent从demo推到生产最大的敌人不是模型能力不够而是失控。总结三个必须做的基础工程措施第一工具返回值必须结构化、短小、带状态标识。不要返回一大段HTML让模型自己解析尽量返回JSON加上错误码。模型不擅长在垃圾数据里找关键信息但它很擅长在清晰的结构化数据里做决策。第二关键操作加人工确认。比如Agent要批量发送邮件、执行数据库删除操作、调支付接口这类动作必须设置二次确认环节。实现上可以在Agent内部先返回“准备执行XX操作”的状态由前端或审批流确认后再真正执行。第三全链路日志。每一轮Agent的thought模型在想什么、tool_input决定调什么工具、tool_output工具返回什么都必须记录下来。这个日志在开发期是调试利器上线后也是审计依据。我见过太多Agent项目出问题后连“它刚才到底做了什么”都查不到排查成本极高。常见坑典型表现处理办法工具返回格式不规范模型反复调用同一工具docstring写清楚返回值结构化成JSON参数乱传把订单号传成日期给参数加正则校验和更严格的description死循环反复调用工具不输出结论设置max_iterations工具返回“最终结果”语义连续多轮后上下文爆炸运行越来越慢、token超限定期压缩记忆只保留摘要和最近N轮模型指令跟随能力弱明明不该调工具却乱调Agent场景优先选指令跟随强的模型不要盲目用最低配6. 进阶方案Agentic RAG与LangGraph的工程化整合6.1 普通RAG不够用在哪里普通RAG的流程是固定不变的拿到问题、检索、拼上下文、生成。它的问题在于“不管什么问题都走同一套路”——用户问“今天星期几”也要去向量库捞一通问一个跨多个文档的复杂问题又只检索一次就草草回答。Agentic RAG的核心区别是把RAG的各个动作变成Agent可以调度的工具让模型自主决定这个问题需不需要检索需要检索几次第一次检索结果够不够要不要换个关键词再查如果答案可信度低是继续查还是反问用户举个例子用户问“去年Q4华东区销售额下滑的主要原因是什么”。普通RAG拿这个问题去检索可能只捞到一份销售汇总文档里的几个片断。Agentic RAG则会拆分成多个动作先检索销售汇总确定下滑事实再检索区域报告找华东区细节如果信息不足还会检索市场环境分析做交叉验证最后生成结论并标注每个结论的依据来源。6.2 LangGraph状态机实现与PGVector存储方案LangGraph实现Agentic RAG的思路非常清晰把流程定义成一个状态图每个节点是“决策”或“执行动作”节点之间通过状态传递数据。我的一个典型设计是三个节点决策节点接收用户问题让模型判断是直接回答、需要检索还是需要反问澄清。工具节点执行检索调用向量库查询。生成节点基于所有上下文生成最终答案给出引用来源。简化版代码如下from typing import TypedDict, Literal from langgraph.graph import StateGraph, END class AgentState(TypedDict): question: str context: list answer: str def decide(state: AgentState) - AgentState: # 让模型判断是否检索 return state def retrieve(state: AgentState) - AgentState: docs retriever.invoke(state[question]) state[context] docs return state def generate(state: AgentState) - AgentState: context_text format_docs(state[context]) state[answer] rag_chain.invoke({ context: context_text, question: state[question] }) return state graph StateGraph(AgentState) graph.add_node(decide, decide) graph.add_node(retrieve, retrieve) graph.add_node(generate, generate) graph.add_edge(decide, retrieve) graph.add_edge(retrieve, generate) graph.add_edge(generate, END) app graph.compile()实际项目中decide节点里会让模型输出一个结构化判定结果比如{action: retrieve, reason: 需要查询内部资料}或{action: answer }然后根据这个结果做条件路由也就是LangGraph里的add_conditional_edges。这样做的好处是流程图一目了然每次调用走哪条路径都有迹可循运维和排障体验比一段黑盒Agent循环好太多。PGVector在这套方案里的角色是向量存储层。它的核心优势在于支持SQL语义可以按文档来源、部门、时间等元数据做过滤。比如内部知识库只需要检索“HR部门”的文件或者只查“2024年之后发布的制度”一条metadata_filter就搞定了这个能力在纯向量数据库里实现起来反而麻烦。6.3 技术热词背后的思考Agent、Skill、Harness现在社区里热词一个接一个今天Agent明天Skill后天Harness。我第一次看到这些概念时也一度混乱后来画了一条清晰的线Agent是决策主体它负责理解目标、规划动作、判断何时终止Skill是能力封装把一个具体的业务流程或工具调用打包成可复用的技能包Harness是运行框架负责调度Agent、管理生命周期、处理环境和安全边界。理解了这个边界你就不会被新概念带跑。同样看到“LangChain和LangGraph都已经过时了吗”这种讨论也不用慌底层框架迭代很快但“问题拆解、外部知识注入、工具调用、状态管理”这四件事的思维方式不会过时。框架只是表达这些思想的不同形态真正值钱的是你做项目时的拆解能力和工程手感。7. 部署实践与常见问题排查7.1 模型部署与接口替换部署这块企业项目通常两条路。第一条最简单直接调用云厂商的大模型API按token付费省去GPU运维成本。第二条是私有化部署开源模型数据不出内网用vLLM做推理服务。vLLM是目前最常用的开源推理框架因为它吞吐量高、显存管理好而且原生提供OpenAI兼容接口这意味着LangChain基本不用改代码只把base_url指向vLLM的地址就行。启动一个模型的命令大概是vllm serve Qwen/Qwen2.5-7B-Instruct \ --host 0.0.0.0 \ --port 8000 \ --served-model-name qwen2.5-7b \ --gpu-memory-utilization 0.9硬件配置方面我实测过的经验值7B模型做FP16推理大约需要14到16GB显存做AWQ或GPTQ量化后需求量可以降到8GB以下14B模型量化后24GB显存够用70B级别就是多卡方案了。预算有限的团队可以从7B或14B起步结合量化方案先跑通业务闭环后续再评估是否升级。如果是调用云API记得在代码里把model参数和base_url改成对应厂商的值。国内可用的模型服务和开源社区的中文模型选择都比较丰富做到这一步LangChain的模型层就彻底解耦了。7.2 问题排查速查表与独家经验最后把我在多个项目里整理出的问题排查表放出来遇到问题先对照看现象可能原因排查 / 解决检索结果为空chunk切太大或太小、Embedding模型不匹配、score阈值过高检查召回片段的相似度分数分布换bge-m3类中文模型适当降低阈值答案幻觉严重Prompt没有强制“仅依据材料”收紧Prompt加上“未找到就拒绝回答”的兜底做Rerank提高片段相关性检索到但答不对上下文拼装顺序有问题、关键片段被截断检查format_docs输出调大chunk_size或overlapAgent反复调用同一工具工具返回信息不足或格式混乱、docstring描述不清结构化工具返回值在docstring里写清适用条件和典型示例Agent卡死或超时max_iterations太大、模型在无意义循环设置迭代上限5-10次为工具增加“无变化则终止”的逻辑工具参数传错参数类型或约束未定义清楚使用Pydantic定义工具入参Schema给参数加正则可以校验上下文超长检索出的片段过多、对话历史过长精简Prompt模板先摘要再检索扩大上下文窗口或分段处理向量检索慢未建索引、数据量大但用了暴力检索FAISS换IVF/HNSW索引PGVector建IVFFlat或HNSW索引并发下响应慢每次请求都做完整链路加缓存热点问题走固定答案异步化Embedding和检索除了这张表还有一条我特别想分享的经验生产环境的RAG系统一定要分环境跑评估。上线前在测试集上测指标上线后监控两个线上指标——检索无结果率用户提问但检索不到可用片段的比例和引用无效率模型给出的引用和答案内容对不上的比例。这两个指标最能真实反映知识库的健康度。我亲眼见过很多团队花大力气上线知识库上线后却从不看检索日志直到用户吐槽“总是找不到”才发现知识库里新增的文档根本没进索引。写在最后这套项目做下来我的体会是学RAG和Agent最忌讳的就是只看概念、不动手。把最小demo跑起来只需要半天但把系统调到稳定需要连续几天和badcase搏斗。先拿自己手头的工作文档建一个几十页的小知识库再给Agent接第一个工具这种从0到1的实感比任何教程都管用。最后一个建议也是我每次新项目都会坚持的第一条工程规范日志里永远要把“用户的问题、检索到的材料、模型给出的回答”三段放在一起记录。等你要优化效果、定位问题时就知道这个习惯有多值钱了。