做简历项目最怕什么?不是写得不够多,而是面试官一追问就露馅。
很多同学的大模型项目是这样写的:“独立开发了基于 LangChain 的企业知识库问答系统,通过 RAG 解决了大模型幻觉问题,上线后效果良好。”看起来完整,实际一问就卡住:数据是怎么清洗的?切分策略为什么这么定?向量检索用了什么距离,召回几篇?回答不忠实怎么评价?
本文要讲的不只是一个“能跑起来”的 demo,而是一条可以直接写进简历的大模型岗位实战链路:RAG 知识库问答 + 智能客服 Agent + 多 Agent 协作。项目包含工程落地必需的数据处理、检索增强、工具调用、质量测评和错误排查。读完你能获得三样东西:
- 一套可以逐步复现的项目源码结构;
- 每个模块的关键取舍和面试官追问点;
- 保证简历经得起深挖的细节边界。 先说结论:这类项目的核心价值不在“你会不会调 LangChain 的 API”,而在于你是否能回答——知识怎么切、检索怎么评、Agent 怎么和真实业务系统协作。这才是大模型岗位真正在意的工程能力。
1. 这篇文章真正要解决的问题
1.1 大模型岗位项目经历,面试官到底在看什么
大模型应用开发岗位这几年很热,但面试官筛选候选人的逻辑没有变:你做系统时有没有清晰的边界判断能力。
同样是“我做过 RAG”,有人只会在 Jupyter Notebook 里vectorstore.similarity_search,有人能说清楚:
- 知识库数据来自哪几种格式,清洗时做了什么;
- chunk_size 为什么是 400 而不是 2000;
- embedding 用的是什么模型,为什么不在线直接用 OpenAI 的 text-embedding;
- 检索结果是按什么距离排序的,是 dense vector search 还是 BM25 混合检索;
- top_k 取几,上下文拼出来有多长;
- 模型回答“不忠实于材料”时,怎么从指标上发现,而不是靠感觉。
后一种人,即使代码写得不算花哨,面试官也更愿意要。因为真实业务里的大模型应用,本质上就是一连串需要做判断的工程问题。
1.2 为什么选“知识库问答 + 智能客服 + 多 Agent”这个组合
知识库问答是 RAG 最基础的场景,用来训练你处理数据、检索、生成三条链路;智能客服需要把 RAG 能力和工具调用结合,让模型不再只会“说”,而是能“做”;多 Agent 协作则把问题复杂度再推一层,让模型面对“查订单 + 判断退款规则 + 回复用户”这类组合任务时,知道怎么拆解和执行。
三个模块难度是递增的,对应大模型岗位的三类常用能力:
| 模块 | 核心能力 | 简历上可以写成的关键词 |
|---|---|---|
| 知识库问答 | RAG 流程设计、向量检索、prompt 约束 | RAG、Embedding、Faiss、召回评估 |
| 智能客服 | Function Calling、工具调用、业务接口集成 | Agent、Tool Use、对话状态管理 |
| 多 Agent 协作 | 路由、编排、状态流转、质量校验 | LangGraph、多 Agent、反思机制、Agentic RAG |
有一点要提醒:这类项目并不代表你完成了生产环境的全部工作。真实上线的系统还需要统一的身份鉴权、日志追踪、模型成本和容灾方案。本文先带你打通最小可用闭环,把“面试会问但普通教程不讲”的部分补齐。
2. RAG、LangChain、Agent 到底分别解决什么问题
2.1 RAG:给大模型配一个允许随时翻书的助手
大模型的知识在训练完成后就固定了,而且内部知识无法追溯到具体出处。企业场景里,产品文档、售后政策、内部制度往往每天变化,模型不可能及时知道。
RAG 的做法是:用户提问时,先从外部知识库里检索相关内容,把检索到的片段拼进 prompt,再让模型参考这些材料回答。它最直接的价值有两个:
- 解决知识时效性,不需要反复重新训练模型;
- 降低幻觉概率,因为模型有了明确可引用的信息来源。
RAG 不是神秘的技术,它四条链路很清楚:文档加载 -> 文本切分 -> Embedding 向量化并建索引 -> 检索增强生成。这里的“检索”,主流实现就是 dense vector search:把文本映射为稠密向量,用向量相似度找到最相关的候选片段。
工程化时真正麻烦的是前两步:数据格式乱七八糟、切分后语义被截断、Embedding 模型没选好。后面章节会逐个处理。
2.2 Agent:让模型从“只说不做”变成“能调用、能决策”
RAG 只能回答知识类问题。如果用户问“帮我查一下订单到哪了”,知识库里不可能也不应该存某个用户的订单物流,因为这是实时数据,必须通过业务接口拿到。
这时需要 Agent。一个具备了工具调用能力的 Agent,工作方式可以理解为三步循环:
- 理解用户意图;
- 决定调用哪个工具、传入什么参数;
- 拿到工具返回结果后,继续组织最终回答。
比如用户说“我买了你的耳机,现在想退货,订单号是 A2024001”。纯 RAG 模型会去知识库里找“退货政策”;带了工具能力的 Agent 会先通过订单工具确认订单状态,再结合退货政策给出结论。
判断一个项目是否需要引入 Agent,不需要看概念多高级,只看一条:用户任务里是否需要做实时查询或状态变更。不需要,就老老实实用 RAG;需要,再考虑 Agent。
2.3 LangChain 和 LangGraph 的真正区别
搜索热词里最常见的问题是“LangChain 和 LangGraph 的区别”。很多同学理解成新框架替代老框架,这是误区。
LangChain 的核心价值是做组件抽象:文档加载器、文本切分器、向量库、模型等都能统一成接口,然后用一条固定的链串联。适合流程明确、不需要修改执行路径的任务,比如标准 RAG。
LangGraph 解决的是 LangChain 在“有状态、循环、分支”场景里的不自然。它的核心是一个可执行的图:每个节点是一个函数,节点之间的边可以是顺序、条件或循环。多 Agent 场景里常有“先检索,质量不行就改写再检索”这种循环,用 LangGraph 表达比手动维护 while 循环要清晰得多。
用一句话记忆:
- LangChain 是把零件拼成流水线;
- LangGraph 是把流水线升级成一张可以回头的路网。
本文以 LangChain 风格的组件作为基础代码,在多 Agent 部分给出类似 LangGraph 的编排思路。这样即使你不熟悉某个具体 API,也能理解它的运行逻辑。
3. 项目技术选型与整体架构设计
3.1 三大模块怎么协作
完整的项目可以放在一个仓库里,目录上按数据层、索引层、服务层、Agent 层拆分,避免面试时被问到“项目结构怎么分层”时回答不出来。
三个模块之间不追求微服务化,重点是让代码清晰体现能力分层。模块一实现基础检索问答,模块二在模块一基础上加工具调用,模块三把模块一和模块二包装成可以被编排调用的 Agent。
3.2 技术栈选择
为了让项目既能演示又不依赖在线付费服务,建议使用以下组合:
| 组件 | 作用 | 建议 |
|---|---|---|
| Python 3.10+ | 开发语言 | 建议用虚拟环境,不污染全局 |
| LangChain | 文档加载、切分、组件编排 | 用稳定版本即可,本文不依赖过新的实验 API |
| sentence-transformers | 本地 Embedding | 可选用 BAAI/bge-small-zh-v1.5 这类中文模型 |
| Faiss | 向量检索索引 | faiss-cpu 足够启动阶段使用 |
| OpenAI SDK | 调用大模型 | 走 OpenAI 兼容协议,方便切换不同服务 |
| FastAPI | 可选,做服务接口 | 项目成熟后可把内部函数暴露成 HTTP API |
大模型接口统一采用 OpenAI 兼容协议,通过环境变量配置base_url、api_key、model,正式代码里不要硬编码。版本细节以你实际安装为准,重点是理解流程。
3.3 项目目录设计与环境变量
项目目录建议这样设计:
rag-agent-project/ ├── data/ │ ├── docs/ # 原始知识文档,放 md/txt/pdf │ └── faiss_index.bin # 向量索引文件 ├── src/ │ ├── loader.py # 加载与切分 │ ├── vector_index.py # 构建索引 │ ├── retriever.py # 检索逻辑 │ ├── llm_client.py # 统一的大模型访问封装 │ ├── rag_pipeline.py # 模块一:RAG 问答 │ ├── agent_customer.py # 模块二:智能客服 Agent │ └── agent_multi.py # 模块三:多 Agent 协作 ├── eval/ │ └── eval_retrieval.py # 召回评估脚本 ├── .env.example └── requirements.txt`.
环境变量文件.env.example如下:
LLM_BASE_URL=https://your-llm-service.com/v1 LLM_API_KEY=sk-your-key LLM_MODEL=your-model-name EMBEDDING_MODEL=BAAI/bge-small-zh-v1.5这样的设计在简历里很容易写成一句有说服力的话:“项目支持通过环境变量切换不同模型服务,知识索引与推理服务解耦。”这句话比“我搭建了大模型问答系统”专业得多。
4. 模块一:基于 RAG 的知识库问答实现
4.1 文档加载与文本切分:为什么不能直接整篇丢给模型
很多同学做 RAG 时直接拿整份 PDF 去窗口里做相似度搜索,效果很差。因为大模型上下文有限,而且用户问题往往只涉及文档中的一小块内容。检索单位太大,向量会被无关内容稀释,命中率自然不高。
正确的做法是把文档切成长度适中的片段,同时保留语义完整性。LangChain 的RecursiveCharacterTextSplitter是常用的切分器,它优先按段落分隔符切,再按句子标点切,尽量不让一句话被硬生生截断。
下面的示例代码以 markdown 和 txt 文档为例。如果你的原始数据是 PDF,可以换成 PDF 加载器。
# src/loader.py from pathlib import Path from langchain_community.document_loaders import TextLoader from langchain.text_splitter import RecursiveCharacterTextSplitter def load_and_split(source_dir: str = "data/docs", chunk_size: int = 500, chunk_overlap: int = 50): """加载目录下的文档并切分成 chunk""" docs = [] for file_path in Path(source_dir).glob("*.md"): loader = TextLoader(str(file_path), encoding="utf-8") docs.extend(loader.load()) splitter = RecursiveCharacterTextSplitter( chunk_size=chunk_size, chunk_overlap=chunk_overlap, separators=["\n\n", "\n", "。", "!", "?", ";", " ", ""], ) chunks = splitter.split_documents(docs) print(f"切分后文档块数量: {len(chunks)}") return chunks这里有两个重点:
chunk_overlap不能随便设成 0。相邻片段做少量重叠,可以避免一个完整知识点刚好被切到两个片段边界而检索不到。separators里加入中文标点,比默认的英文分隔符对中文更友好。
切分长度要综合考虑你的 Embedding 模型支持长度、知识文档的结构、用户问题的颗粒度。如果只是做 demo,先跑通,后面再根据评测调参。
4.2 Embedding 与向量索引构建
文本切完后,需要把每个片段转成向量。这里选用本地 sentence-transformers 模型,优点是离线、可控、适合项目演示,也方便后续扩展成特定的领域语义模型。
向量索引采用 Faiss。Faiss 支持多种索引,启动阶段用IndexFlatIP(内积)或IndexFlatL2(欧氏距离)即可。经过归一化后,使用内积等同于计算余弦相似度。
# src/vector_index.py import json import faiss import numpy as np from sentence_transformers import SentenceTransformer INDEX_PATH = "data/faiss_index.bin" META_PATH = "data/chunk_meta.json" def build_index(chunks, model_name: str = "BAAI/bge-small-zh-v1.5"): """将文档片段向量化并写入本地 Faiss 索引""" model = SentenceTransformer(model_name) texts = [c.page_content for c in chunks] metas = [{"page_content": c.page_content, **c.metadata} for c in chunks] embeddings = model.encode(texts, normalize_embeddings=True) embeddings = np.asarray(embeddings, dtype="float32") dim = embeddings.shape[1] index = faiss.IndexFlatIP(dim) index.add(embeddings) faiss.write_index(index, INDEX_PATH) with open(META_PATH, "w", encoding="utf-8") as f: json.dump(metas, f, ensure_ascii=False, indent=2) print(f"索引已保存: {INDEX_PATH}, 共 {len(texts)} 条")调用方式:
from src.loader import load_and_split from src.vector_index import build_index chunks = load_and_split() build_index(chunks)normalize_embeddings=True这一步很关键。如果不做归一化,IndexFlatIP 受向量模长影响,检索结果的可解释性会变差。Faiss 索引和 metadata 建议分开存储,索引里只放向量,文本内容放 JSON,后续更新文档时不用重新写入全部向量。
4.3 检索与答案生成:自己写检索反而更可控
项目里不直接使用黑盒的create_retrieval_chain,而是手写检索函数。原因很简单:面试官问“你的 top_k 怎么定的”,你不能只能说“框架默认值”。自己写检索函数,参数和数据流向一目了然。
# src/retriever.py import json import faiss import numpy as np from sentence_transformers import SentenceTransformer INDEX_PATH = "data/faiss_index.bin" META_PATH = "data/chunk_meta.json" MODEL_NAME = "BAAI/bge-small-zh-v1.5" _index = None _meta = None _model = None def _load_resource(): global _index, _meta, _model if _index is None: _index = faiss.read_index(INDEX_PATH) if _meta is None: with open(META_PATH, "r", encoding="utf-8") as f: _meta = json.load(f) if _model is None: _model = SentenceTransformer(MODEL_NAME) return _index, _meta, _model def search(query: str, top_k: int = 4): """返回 (命中文档列表, 相似度分数列表)""" index, meta, model = _load_resource() q_vec = model.encode([query], normalize_embeddings=True) q_vec = np.asarray(q_vec, dtype="float32") scores, ids = index.search(q_vec, top_k) docs = [meta[i]["page_content"] for i in ids[0]] return docs, scores.tolist()[0]生成阶段,我们统一用 OpenAI 兼容 client 去调用模型。这样代码不绑定某一家模型,Switch 起来很方便。
# src/llm_client.py import os from openai import OpenAI _client = None def get_client(): global _client if _client is None: _client = OpenAI( base_url=os.getenv("LLM_BASE_URL"), api_key=os.getenv("LLM_API_KEY"), ) return _client def chat(messages, temperature: float = 0.2): client = get_client() resp = client.chat.completions.create( model=os.getenv("LLM_MODEL"), messages=messages, temperature=temperature, ) return resp.choices[0].message.contentRAG 的主流程是:先检索,再把检索结果拼进 system prompt,让模型明确只能引用材料回答。
# src/rag_pipeline.py from src.retriever import search from src.llm_client import chat def rag_answer(question: str, top_k: int = 4): docs, scores = search(question, top_k=top_k) context = "\n\n---\n\n".join(docs) messages = [ { "role": "system", "content": ( "你是企业知识库问答助手。请严格根据提供的资料回答问题;\n" "如果资料中没有相关信息,直接回答“资料中未找到相关内容”,不要编造。\n" "回答时优先引用原文口径。" ), }, { "role": "user", "content": f"资料如下:\n{context}\n\n问题:{question}", }, ] answer = chat(messages) return {"answer": answer, "source_docs": docs, "scores": scores}这个环节你会注意到:我们并没有依赖 LangChain 的 chain 对象,而是用了最直白的函数。原因是核心链路应该由自己控制,LangChain 的价值在于前期的文档加载、切分,以及后续如果你需要把它接入更大编排体系时的标准化表达。
4.4 最小验证
写好上面代码后,准备一个data/docs/product_help.md,内容随便写几条产品 FAQ,比如“退换货政策”“保修期多久”“发货时间”。然后运行:
python -c "from src.rag_pipeline import rag_answer; print(rag_answer('产品保修期是多久'))"预期效果:答案里能看到知识库中写明的保修期限,同时source_docs里返回命中的原文片段。
如何判断成功:先别看答案流畅不流畅,先检查检索命中文档里是否包含正确的原文片段。如果检索就错了,后面生成再流畅也是错的。检索正确后,再看回答有没有加入材料之外的信息。
5. 模块二:智能客服 Agent 与 Function Calling
5.1 为什么纯 RAG 做不了合格的客服
如果把模块一直接接到客服场景,会出现一个很尴尬的咨询:“我的订单为什么还没发货?”知识库里有“发货时间一般是 48 小时”,但用户的订单状态在订单系统里,模型完全不知道这个订单是已支付、待发货,还是已发货。
此时模型只能含糊回答“请您耐心等待”。真实客服需要的是实时查订单,然后结合物流知识给结论。
这里就需要 Function Calling。它不是让模型自己去访问数据库,而是让模型从用户话里提取参数,然后决定调用哪个函数。真正执行函数的是我们的可信代码,结果再交回模型组织语言。
5.2 定义订单查询工具
先写一个工具函数。出于安全考虑,这里用 mock 数据模拟真实业务接口,真实项目中这个函数内部应该调用经过鉴权的订单服务。
# src/order_tool.py import json def get_order_status(order_id: str) -> str: """ 模拟订单系统接口。 真实项目里应在这里调用内部订单 API, 并确保当前用户对该订单有访问权限。 """ mock_order = { "A2024001": { "order_id": "A2024001", "status": "已发货", "courier": "顺丰速运", "tracking_no": "SF1234567890", "logistics": "包裹正在运输途中", }, "A2024002": { "order_id": "A2024002", "status": "退款中", "courier": "", "tracking_no": "", "logistics": "退款申请已提交,预计1-3个工作日到账", }, } order = mock_order.get(order_id) if not order: return json.dumps({"error": "未查询到该订单"}, ensure_ascii=False) return json.dumps(order, ensure_ascii=False)注意:当真实接入订单系统时,必须做权限校验。比如用户 A 不能通过构造order_id查到用户 B 的订单详情。这是 Agent 工具落地的安全红线。
5.3 组装 Function Calling 的主流程
下面的代码演示一个最小但完整的 Function Calling 流程:模型先返回tool_calls,我们再执行对应工具,再把工具结果作为新的消息传回模型。
# src/agent_customer.py import json import os from openai import OpenAI from src.order_tool import get_order_status client = OpenAI( base_url=os.getenv("LLM_BASE_URL"), api_key=os.getenv("LLM_API_KEY"), ) model = os.getenv("LLM_MODEL") TOOLS = [ { "type": "function", "function": { "name": "get_order_status", "description": "根据订单号查询订单当前状态和物流信息", "parameters": { "type": "object", "properties": { "order_id": { "type": "string", "description": "用户提供的订单号", } }, "required": ["order_id"], }, }, } ] def customer_agent(user_text: str) -> str: messages = [ { "role": "system", "content": ( "你是电商智能客服。你可以查询订单状态。" "当用户询问订单物流时,先使用工具查询。" ), }, {"role": "user", "content": user_text}, ] resp = client.chat.completions.create( model=model, messages=messages, tools=TOOLS, ) msg = resp.choices[0].message if not getattr(msg, "tool_calls", None): return msg.content or "" # 模型决定调用工具,执行并回传结果 tool_calls = msg.tool_calls messages.append(msg) for tc in tool_calls: if tc.function.name == "get_order_status": args = json.loads(tc.function.arguments) result = get_order_status(args["order_id"]) messages.append({ "role": "tool", "tool_call_id": tc.id, "content": result, }) final_resp = client.chat.completions.create( model=model, messages=messages, ) return final_resp.choices[0].message.content需要特别强调的是 Tool 消息里的tool_call_id必须与助手消息中模型返回的tc.id一致。很多时候 function calling 报错,不是工具写错,而是消息顺序和 id 没对上。
调用方式:
from src.agent_customer import customer_agent print(customer_agent("你好,帮我查一下订单 A2024001 到哪了"))预期输出:模型不仅能说“您的订单已发货”,还会结合工具返回的物流公司、运单号说明“正在运输途中”。
这一段的面试考点是:
- Function Calling 的执行是模型决定、代码执行,不要在大模型侧传敏感参数;
- 所有工具函数都要做输入校验;
- 工具返回结果必须先被模型看到,再由模型组织回答。
6. 模块三:多 Agent 协作与 Agentic RAG
6.1 什么时候需要多 Agent
很多教程把多 Agent 包装得很玄,好像多个模型角色在里面互相聊天就是多 Agent。实际项目里滥用会带来三个问题:调用成本成倍增加、错误更难追踪、回复延迟明显上升。
什么时候才值得用多 Agent?我认为需要满足两个条件:
- 用户任务可以被清晰拆成多个有边界的子任务;
- 每个子任务分别依赖不同的工具或知识库,且有验证中间结果的需要。
举个具体场景:用户问“我买的耳机有点问题,申请退款的话,订单已经发货了,还能退吗?”
简单 RAG 能回答的是:知识库里写了退货政策。简单 Agent 能回答的是:当前订单状态。但完整回答需要模型先判断“订单状态是否影响退货政策适用范围”,然后把“订单已发货 + 政策允许 7 天无理由退货”两个信息组合起来。这个组合过程如果一次输错,很难定位是检索环节错了,还是模型判断环节错了。
所以多 Agent 的核心不是“用多个模型聊天”,而是把流程拆成多个可独立检查的节点。每个节点负责一件事,节点之间用显式规则或另一个调度模型连接。
6.2 supervisor + worker 的落地设计
项目里最值得实现的设计是 supervisor + worker,主管 Agent 负责理解用户请求,然后把任务路由到不同 worker:
- 知识库 worker:负责检索产品手册和售后政策;
- 订单 worker:负责查询实时订单状态;
- 质检 worker:负责检查最终回答是否忠实于材料。
如果团队刚开始落地,不一定要用复杂图引擎。用普通 Python 函数也可以先跑通一个“检索 -> 生成 -> 质检 -> 反馈改进”的循环。这个循环在业内常被称为 Agentic RAG,比直接 RAG 多出的部分就是“模型自己判断答案质量不好时,可以改写检索词再来一轮”。
下面给一个轻量但面试会认可的实现:
# src/agent_multi.py from src.retriever import search from src.llm_client import chat def generate_draft(question: str, context: str) -> str: messages = [ { "role": "system", "content": "你是知识库问答 Agent,严格根据给定材料回答问题。", }, { "role": "user", "content": f"材料:{context}\n\n问题:{question}", }, ] return chat(messages) def judge_answer(question: str, draft: str, context: str): """质检 Agent:判断答案是否忠实于材料,并输出结构化结论""" messages = [ { "role": "system", "content": ( "你是质检 Agent。请判断下面的回答是否完全基于给定材料," "不要要求回答包含材料外的内容。" "输出 JSON:{\"pass\": true/false, \"reason\": \"原因\"}" ), }, { "role": "user", "content": f"材料:{context}\n\n问题:{question}\n\n回答:{draft}", }, ] result = chat(messages, temperature=0) return result def rewrite_query(question: str, reason: str) -> str: """改写 Agent:根据质检反馈把问题改写得更利于检索""" messages = [ { "role": "system", "content": "你是检索改写 Agent,根据反馈把原问题改写成更适合知识库检索的形式,只输出改写后的问题。", }, { "role": "user", "content": f"原问题:{question}\n\n质检反馈:{reason}", }, ] return chat(messages, temperature=0) def multi_agent_resolve(question: str, max_rounds: int = 3): query = question best_draft = "" for round_idx in range(max_rounds): docs, _ = search(query, top_k=5) context = "\n\n---\n\n".join(docs) draft = generate_draft(question, context) best_draft = draft verdict = judge_answer(question, draft, context) if '"pass": true' in verdict or '"pass": True' in verdict: return {"answer": draft, "rounds": round_idx + 1, "verdict": verdict} rewrite_reason = "检索结果可能不完整,需要换关键词重新检索" query = rewrite_query(question, rewrite_reason) return {"answer": best_draft, "rounds": max_rounds, "verdict": "reach max rounds"}这个实现虽然不复杂,却展示了多 Agent 最核心的价值:答案不是一次性生成的,而是经过“生成 -> 质检 -> 改写 -> 再检索”的闭环。简历上写“引入质检 Agent,对回答进行 faithfulness 校验,失败时自动改写查询词循环检索,最终将无效回答比例降低”,比空泛地写“使用 LangGraph 实现多 Agent”有说服力得多。
真实项目如果准备上 LangGraph,以上内容正好可以映射成图上的节点和条件边:检索节点 -> 生成节点 -> 质检节点 -> 条件边(通过进返回,不通过进改写节点)。这也是 LangGraph 与 LangChain 最直观的区别:LangChain 适合固定直线流程,LangGraph 适合这种会回头的循环流程。
7. 怎么评测 RAG 效果,避免简历项目被问倒
7.1 没有指标,项目就只能停留在“调通 demo”
很多同学做完 RAG 后只给出一句“效果不错”。面试官继续追问“怎么评价效果不错”,就卡住了。原因在于,RAG 的好坏不能靠肉眼判断一两轮对话。
项目至少要能回答三类问题:
- 检索准不准——相关片段有没有被召回;
- 生成忠实不忠实——模型有没有照着材料回答;
- 答案能不能解决用户问题。
你可以先做一个检索集来评估召回效果。准备一个简单的测试集,格式如下:
question,expected_content 产品保修期是多久,保修期为一年 如何申请退款,在订单详情页点击申请退款 发货一般需要几天,48小时内发货再写一个计算命中率的脚本:
# eval/eval_retrieval.py import csv from src.retriever import search def load_questions(path: str): with open(path, "r", encoding="utf-8") as f: return list(csv.DictReader(f)) def eval_hit_rate(path: str, top_k: int = 4): data = load_questions(path) hit = 0 for row in data: docs, _ = search(row["question"], top_k=top_k) joined = "\n".join(docs) if row["expected_content"] in joined: hit += 1 print(f"Hit@{top_k}: {hit / len(data):.2%}")这个脚本的价值是让你能回答“top_k 为什么要设为 4”。你可以跑top_k=2和top_k=4两组数据看命中率变化,说明你做过参数实验。
7.2 理解 RAG 知识库的核心指标
更专业的 RAG 评测框架会输出多类指标,建议理解并写到简历里。比如:
| 指标分类 | 直观含义 | 回答什么问题 |
|---|---|---|
| Context Precision | 检索结果里有多大比例是必要的 | 给大模型的材料有没有夹带太多无关内容 |
| Context Recall | 该召回的上下文是否真的被召回了 | 漏关键材料没有 |
| Faithfulness | 生成内容是否忠实于检索材料 | 大模型有没有自己编造 |
| Answer Relevance | 回答是否切题 | 模型答非所问 |
| Answer Correctness | 回答与标准答案是否一致 | 最终业务结论对不对 |
项目中即使只跑通 Hit Rate,也应该能讲清楚上表每一个词。面试官考察的不是你“用过 RAGAS”,而是你能否解释“哪些指标衡量检索、哪些指标衡量生成”。
需要提醒:不要在生产环境拿用户真实问题直接灌给在线评测服务。涉及业务数据时,优先基于脱敏后的测试集做离线评测。
8. 常见问题与排查思路
| 问题现象 | 可能原因 | 排查方式 | 解决方案 |
|---|---|---|---|
| 检索结果明显不对,答非所问 | Embedding 模型与领域不适配 | 打印返回的source_docs,人工检查语义 | 换成中文领域效果更好的 embedding 模型,或引入 BM25 混合检索 |
| 模型总把知识库没有的内容写成“已知” | Prompt 没有强制约束 | 检查 prompt 里有没有“只能根据材料回答”的指令 | 明确 system prompt,让模型不知道时直说“资料中未找到” |
| 切分后句子被截断、上下文割裂 | chunk_size 太小或分隔符不合适 | 查看相邻 chunk 的末尾与开头 | 调整 chunk_size 和 overlap,把中文标点加入 separators |
| 设置了相同参数但效果不稳 | 模型 temperature 过高 | 记录多次输出对比 | 知识问答场景 temperature 调低,比如 0.1 到 0.3 |
| Function Calling 没有触发 | 工具描述不清晰或模型不支持 | 打印模型返回的完整响应 | 检查工具描述是否包含足够触发条件,换支持函数调用的模型服务 |
| tool_call_id 报错 | 消息顺序不对 | 打印 messages 列表 | 确保 assistant tool_calls 消息在 tool 消息之前,且 id 一一对应 |
| 更换公司模型后 API 经常 401 | 环境变量没生效 | 检查启动环境里的 key | 使用.env统一管理,禁止提交明文 key 到代码仓库 |
8.1 在线模型效果差,要不要先排查这几个环节
不要一上来就换大模型。建议顺序是:先看检索回来的文本对不对,再看 prompt 约束够不够,最后才看模型本身。RAG 项目里,检索决定了回答的上限,生成只是把上限尽量兑现。
另外在做数据清洗时,要注意删除文档里的导航栏、版权声明、广告等噪声,否则这些文本会被向量化并干扰检索。这个动作不需要很高技术含量,但决定项目效果下限。
9. 怎么写进简历,以及下一步怎么继续深入
如果你决定把本项目的代码完整整理出来,简历里可以参考下面这种“动作 + 方法 + 结果”的句式,每条都对应实际代码,面试被追问也有据可答:
- 设计并实现企业知识库 RAG 问答系统,完成文档加载、中文文本切分、本地向量索引构建、检索增强生成全流程;
- 基于 Function Calling 开发智能客服 Agent,通过工具调用完成订单状态查询,并形成工具参数校验与结果回传闭环;
- 引入质检 Agent 对模型回答进行 faithfulness 校验,失败时自动改写检索词循环重试,验证了拒答与兜底策略;
- 构建小型离线评测集,统计 Hit@K 召回命中率,并基于该指标调整 chunk_size 和 top_k 参数。
注意不要把自己没做的事写成已上线。简历里写“完成离线 demo”和“上线生产环境”是两回事,面试官更反感的是过度包装。
从学习路径看,下一步可以按以下顺序继续深入:
- 把检索升级为“向量召回 + 关键词召回”的混合检索,观察命中率变化;
- 在编排层尝试用 LangGraph 将多 Agent 流程显式建模,条件边连接质检节点与改写节点;
- 为每个请求记录 trace:query、召回文档、模型输出、耗时、评测判定,形成可观测闭环;
- 如果条件允许,接入真实业务接口时把权限校验做成独立的工具调用层,避免 Agent 裸奔。
这套项目做完后,你对大模型应用开发的认识会更接近“工程系统”,而不是“API 封装工具”。写简历之前,先花一天时间把项目目录整理干净,把.env.example配好,把 README 写清楚。项目质量,往往从 README 就能看出一个候选人的