尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

从零手搓Agent:LLM工具调用、RAG检索与Rerank重排实战

从零手搓Agent:LLM工具调用、RAG检索与Rerank重排实战 1. 为什么我要从零手搓一个Agent先说结论如果你打算认真搞Agent开发别一上来就抱着LangChain、AutoGPT这类框架啃。我见过太多人包括我自己早期花了两周把框架文档翻了个遍结果连一次完整的工具调用链路都跑不通出了问题完全不知道从哪查。那种感觉就像你开着一辆全黑盒的车仪表盘只告诉你“出错了”剩下的全靠猜。Agent这个东西拆开来看其实没那么玄乎。它的本质就是一个循环LLM负责决策代码负责执行执行结果再喂回LLM直到任务完成或触发终止条件。就这么简单。你完全可以用两三百行Python把它从零搭出来而且搭完之后你对整个系统的掌控力是直接用框架永远给不了你的。这篇文章面向的是有一定Python基础、了解LLM基本调用方式、但还没真正动手写过Agent的开发者。我会从最裸的版本开始一步步加上工具调用、记忆管理、RAG检索、Rerank重排最后聊到工程化落地时那些框架不会告诉你的坑。全程不依赖任何重型框架核心逻辑自己写该用库的地方用库但每一行代码你都知道它在干什么。热搜词里那些agent、llm、rag、向量检索、rerank我会在对应的章节里逐个拆解不堆概念只讲我实际踩过的路。2. 先搞清楚Agent到底在干什么2.1 Agent和普通LLM调用的本质区别普通LLM调用是一问一答你给一个prompt模型返回一段文本结束。Agent不一样它是多轮自主决策。你给一个目标模型自己决定下一步做什么调用什么工具拿到结果后判断是否继续直到它认为任务完成。举个具体例子。你问普通LLM“北京今天天气怎么样”它要么说“我无法获取实时信息”要么编一个。但你给Agent同样的任务它会第一步判断需要调用天气查询工具第二步生成工具调用参数城市北京第三步拿到工具返回的真实数据第四步把数据整理成自然语言回复你。这个“判断-调用-整合”的循环就是Agent的核心。听起来简单但工程上的复杂度全在细节里模型怎么知道有哪些工具可用工具调用的参数格式怎么保证正确调用失败了怎么办多轮之后上下文爆了怎么办这些才是真正花时间的地方。2.2 一个最小Agent的组成要素我把一个能跑的Agent拆成四个必需件LLM大脑负责推理和决策。我用的是支持function calling的模型这是前提不支持工具调用的模型做Agent会很痛苦。工具集Agent能调用的外部能力比如搜索、计算、查数据库、调API。每个工具需要清晰的名称、描述和参数schema。循环控制器管理“决策-执行-反馈”的循环包括最大轮数限制、终止条件判断、错误处理。记忆短期记忆就是对话历史长期记忆可以接向量库做RAG检索。这四个件里LLM和工具集是基础循环控制器是骨架记忆是让Agent从“能用”到“好用”的关键。很多人做Agent只做了前三个结果发现Agent记不住之前聊过什么每次都要重新解释背景体验很差。2.3 为什么不用现成框架我不是说框架不好。LangChain、AgentScope这些框架在快速原型阶段确实省事。但问题在于当你需要调试一个诡异的行为时框架的抽象层会让你抓狂。比如模型明明返回了正确的工具调用但框架没执行你翻源码翻了半天发现是某个parser的兼容性问题。这种时间成本在项目紧的时候是致命的。从零手搓的好处是每一层你都能打日志每一个决策你都能复现每一个bug你都能定位到具体行。而且手搓一遍之后你再用框架就知道框架帮你做了什么、哪些地方可能出问题用起来反而更顺手。3. 手搓Agent的核心细节与实操要点3.1 工具定义让模型准确理解你的意图工具定义是Agent开发里最容易被低估的环节。很多人随便写个描述就扔给模型然后抱怨模型调用不对。实际上工具描述的质量直接决定了Agent的可靠性。我用的工具定义格式是JSON Schema每个工具包含name、description、parameters三部分。关键在于description它要回答三个问题这个工具做什么、什么时候用、参数怎么填。举个例子一个查询天气的工具差的描述是“查询天气”好的描述是“根据城市名称查询该城市当前天气状况包括温度、湿度、风力。当用户询问天气相关问题时使用此工具。城市名称需使用中文如‘北京’、‘上海’。”参数定义也要细致。每个参数要有type、description、是否required。对于枚举类型的参数一定要用enum限定取值范围否则模型可能返回一个你根本没处理的选项。注意工具描述不是写给用户看的是写给模型看的。模型只能通过描述来判断何时调用、如何调用。描述里要包含使用场景、参数格式、边界条件。3.2 循环控制什么时候停什么时候继续循环控制是Agent的骨架。最核心的问题是怎么判断Agent该继续还是该停。我的做法是三层判断第一层模型返回的finish_reason。如果模型返回的是tool_calls说明它想调用工具继续循环。如果返回的是stop说明它认为任务完成了输出最终答案。第二层最大轮数限制。我一般设10轮超过就强制终止并返回当前结果。这是防止Agent陷入死循环的兜底。第三层工具执行结果判断。如果工具连续失败三次我会中断循环把错误信息返回给用户而不是让模型继续瞎试。这里有个细节模型有时候会在一次返回里同时包含文本和工具调用。我的处理是如果有工具调用就执行工具文本内容作为中间思考记录如果没有工具调用文本内容就是最终答案。3.3 上下文管理别让历史撑爆你的窗口多轮循环之后对话历史会越来越长。如果不做管理很快就会超出模型的上下文窗口然后你就看到那个经典的报错llm request failed: provider rejected the request schema or tool payload。我的策略是滑动窗口摘要压缩。保留最近N轮完整对话更早的历史用LLM生成一段摘要把摘要作为系统消息放在最前面。这样既保留了关键信息又控制了token数量。具体实现上我设了一个阈值当历史token数超过模型窗口的60%时触发压缩。压缩时把最早的几轮对话拿出来让模型生成一段200字以内的摘要然后把这几轮替换成摘要。实操心得压缩的prompt要明确告诉模型“保留关键事实、决策和结果丢弃寒暄和重复内容”。我试过不指定模型会把“好的”“明白了”这种废话也摘要进去浪费token。3.4 错误处理Agent开发里最脏最累的活错误处理是Agent从demo到可用的分水岭。demo里工具调用失败直接抛异常就完事了但生产环境里你需要考虑网络超时、API限流、参数格式错误、工具返回空结果、模型返回格式不符合预期……我的做法是给每个工具调用包一层try-except把错误信息结构化后返回给模型让模型决定是重试、换工具还是放弃。比如工具返回“参数city不能为空”模型看到后会自动补上城市名重新调用。但这里有个坑如果错误信息太技术化模型可能理解不了。比如“KeyError: city”模型看了也不知道怎么办。所以错误信息要转成自然语言“调用天气查询工具失败原因缺少必要参数‘城市名称’。请补充城市名称后重试。”4. 给Agent加上RAG和Rerank4.1 为什么Agent需要RAGAgent本身只能依赖模型内部的知识和工具返回的信息。但很多场景下你需要Agent基于私有知识库回答问题比如公司内部文档、产品手册、个人笔记。这时候就需要RAG检索增强生成。RAG的核心思路是把知识库文档切块、向量化、存入向量库用户提问时把问题也向量化在向量库里检索最相似的几个块作为上下文喂给LLM。但Agent里的RAG和普通RAG有个关键区别Agent可以自主决定什么时候检索、检索什么。普通RAG是每次提问都检索Agent是把检索封装成一个工具模型判断需要知识库信息时才调用。4.2 向量库选型别一上来就上重型数据库热搜词里有人问“向量库检索需要什么数据库”我的回答是看数据量。数据量在10万条以下用FAISS就够了纯内存零依赖pip装完就能用。数据量在百万级可以考虑Milvus或Qdrant支持持久化和分布式。再往上才需要考虑专门的向量数据库集群。我个人的项目大多在10万条以内FAISS完全够用。它的索引构建快检索延迟在毫秒级而且不需要额外部署服务。对于个人项目和中小团队这是性价比最高的选择。注意FAISS的索引默认在内存里进程重启就没了。生产环境需要定期把索引持久化到磁盘启动时加载。4.3 文档切块切得好不好直接决定检索质量文档切块是RAG里最容易被忽视但影响最大的环节。切得太碎语义不完整切得太粗检索精度下降。我的经验是按语义切不按字数切。具体做法是先用换行符和标点做粗切然后合并相邻的小块直到接近目标长度我一般用500字左右。同时保留一定的重叠overlap我设的是50字防止关键信息刚好被切断。对于结构化文档比如Markdown我会按标题层级切每个二级标题下的内容作为一个块。这样每个块都有明确的主题检索时更容易匹配。4.4 Rerank让检索结果从“能用”到“好用”向量检索有个天然缺陷它基于语义相似度但相似不等于相关。有时候检索出来的top5里真正有用的可能只有第3条但第1条因为字面相似度高被排在了前面。Rerank就是解决这个问题的。它的思路是先用向量检索召回一批候选比如20条然后用一个专门的Rerank模型对这20条做精细排序选出最相关的5条。我用的Rerank方案是交叉编码器Cross-Encoder它把query和document拼在一起输入模型直接输出相关性分数。相比向量检索的双塔结构交叉编码器的精度更高但速度更慢所以只用在召回后的精排阶段。实测下来加了Rerank之后检索结果的相关性提升非常明显。原来top1经常是无关内容现在top1基本都能命中关键信息。4.5 Agentic RAG让Agent自己决定怎么检索Agentic RAG是最近很火的概念核心思想是让Agent自主控制检索过程。具体来说Agent可以判断是否需要检索决定检索什么关键词对检索结果不满意时改写query重新检索结合多个检索结果做综合推理我实现的方式是把检索封装成一个工具同时在系统prompt里告诉模型“当你需要知识库信息时调用search_knowledge工具。如果第一次检索结果不相关尝试用不同的关键词重新检索。”这样Agent就有了自主检索的能力比固定流程的RAG灵活很多。实测在一些复杂问题上Agentic RAG的准确率比普通RAG高出一截。5. 完整实操从零搭一个带RAG的Agent5.1 环境准备与依赖安装我用的技术栈很轻量pip install openai faiss-cpu numpy sentence-transformersopenai调用LLM APIfaiss-cpu向量检索numpy向量运算sentence-transformers生成embedding和Rerank如果你用的是其他LLM提供商把openai换成对应的SDK就行接口逻辑是一样的。5.2 第一步搭建LLM调用层先封装一个统一的LLM调用函数处理重试、超时和错误import openai import time def call_llm(messages, toolsNone, max_retries3): for attempt in range(max_retries): try: response openai.chat.completions.create( modelgpt-4o, messagesmessages, toolstools, temperature0.1 ) return response.choices[0].message except Exception as e: if attempt max_retries - 1: raise time.sleep(2 ** attempt)temperature设0.1是因为Agent需要稳定的决策不需要创意。重试用了指数退避避免短时间内频繁请求。5.3 第二步定义工具集我定义了三个工具天气查询、知识库检索、计算器。tools [ { type: function, function: { name: search_knowledge, description: 在知识库中检索相关信息。当用户询问需要私有知识才能回答的问题时使用。, parameters: { type: object, properties: { query: { type: string, description: 检索关键词使用自然语言 } }, required: [query] } } }, { type: function, function: { name: calculate, description: 执行数学计算。当需要进行数值运算时使用。, parameters: { type: object, properties: { expression: { type: string, description: 数学表达式如 2 3 * 4 } }, required: [expression] } } } ]5.4 第三步实现RAG检索工具这是核心部分包含向量化、检索和Rerankimport faiss import numpy as np from sentence_transformers import SentenceTransformer, CrossEncoder # 初始化模型 embed_model SentenceTransformer(BAAI/bge-small-zh-v1.5) rerank_model CrossEncoder(BAAI/bge-reranker-base) # 构建索引 def build_index(documents): embeddings embed_model.encode(documents, normalize_embeddingsTrue) index faiss.IndexFlatIP(embeddings.shape[1]) index.add(embeddings.astype(float32)) return index, documents # 检索Rerank def search_knowledge(query, index, documents, top_k5, recall_k20): query_vec embed_model.encode([query], normalize_embeddingsTrue) scores, indices index.search(query_vec.astype(float32), recall_k) candidates [documents[i] for i in indices[0]] pairs [[query, doc] for doc in candidates] rerank_scores rerank_model.predict(pairs) ranked sorted(zip(candidates, rerank_scores), keylambda x: x[1], reverseTrue) return [doc for doc, _ in ranked[:top_k]]这里用了BGE系列的中文模型embedding用small版本够快Rerank用base版本精度更好。normalize_embeddingsTrue是为了用内积做余弦相似度。5.5 第四步实现Agent主循环def run_agent(user_input, index, documents, max_turns10): messages [ {role: system, content: 你是一个智能助手可以调用工具来完成任务。需要知识库信息时调用search_knowledge需要计算时调用calculate。}, {role: user, content: user_input} ] for turn in range(max_turns): response call_llm(messages, toolstools) if not response.tool_calls: return response.content messages.append(response) for tool_call in response.tool_calls: name tool_call.function.name args json.loads(tool_call.function.arguments) try: if name search_knowledge: result search_knowledge(args[query], index, documents) result \n---\n.join(result) elif name calculate: result str(eval(args[expression])) else: result f未知工具{name} except Exception as e: result f工具执行失败{str(e)} messages.append({ role: tool, tool_call_id: tool_call.id, content: result }) return 达到最大轮数限制任务未完成。这个循环就是Agent的心脏。每一轮模型决定是否调用工具如果有工具调用执行后把结果追加到消息历史如果没有返回最终答案。5.6 第五步跑起来看看docs [ 公司年假政策入职满一年享受5天年假满三年10天满五年15天。, 报销流程填写报销单附上发票提交给直属领导审批财务审核后打款。, 产品定价基础版99元/月专业版299元/月企业版需联系销售。 ] index, documents build_index(docs) print(run_agent(公司年假有多少天, index, documents)) print(run_agent(专业版多少钱帮我算一下买一年比按月买省多少, index, documents))第二个问题会触发两次工具调用先检索价格再计算年付节省金额。这就是Agent的价值——它能组合多个工具完成复杂任务。6. 工程化落地时踩过的坑6.1 模型返回格式不稳定即使用了function calling模型偶尔还是会返回格式不对的JSON或者参数类型不对。我的处理是在解析时加一层校验如果解析失败把错误信息返回给模型让它重新生成。还有一种情况是模型返回了工具调用但参数是空的。这时候不要直接执行而是把“参数缺失”的信息返回给模型让它补充。6.2 工具调用陷入死循环我遇到过模型反复调用同一个工具每次参数都一样结果也一样但它就是不停。后来加了检测如果连续两次工具调用的name和arguments完全相同就强制中断返回当前结果。6.3 上下文token增长过快RAG检索返回的文档块如果太长会迅速吃掉上下文。我的做法是限制每个块的长度500字以内并且只返回top3而不是top5。如果模型觉得信息不够它会自己再检索一次。6.4 Rerank模型的选择Rerank模型不是越大越好。我试过用large版本的reranker精度确实高一点但延迟从50ms涨到了200ms对于交互式Agent来说体验下降明显。最后选了base版本精度和速度平衡得比较好。6.5 向量库的持久化FAISS索引在内存里进程重启就没了。我的做法是每次更新知识库后把索引和文档一起保存到磁盘faiss.write_index(index, index.faiss) with open(documents.json, w) as f: json.dump(documents, f, ensure_asciiFalse)启动时先检查文件是否存在存在就加载不存在就重新构建。7. 常见问题速查问题可能原因解决方法模型不调用工具工具描述不清晰补充使用场景和参数说明工具调用参数错误参数schema不明确加enum限定、加description检索结果不相关切块太粗或太细调整切块大小加Rerank上下文超限历史太长滑动窗口摘要压缩循环不终止模型反复调用同一工具加重复调用检测工具执行超时外部API慢加超时和重试机制Rerank太慢模型太大换base版本或减少候选数8. 关于Agent开发的一些个人体会手搓Agent这件事最大的收获不是写出了一个能跑的系统而是对整个链路有了肌肉记忆。现在我看到任何一个Agent框架都能快速判断它的抽象层在哪里、可能出问题的地方在哪里。如果你刚开始学Agent开发我的建议是先别碰框架用最裸的方式跑通一个最小闭环。哪怕只有两个工具、一个循环、没有RAG跑通了你就理解了Agent的本质。然后再逐步加上记忆、RAG、Rerank、错误处理每加一个模块都打日志观察行为变化。RAG和Rerank这块不要追求一步到位。先用FAISS加embedding跑通检索发现精度不够再加Rerank发现切块不合理再调切块策略。每一步都有明确的优化目标而不是一上来就堆一堆组件。最后说一个我踩过的坑Agent的prompt不要写太长。我早期喜欢在system prompt里塞一大堆规则结果模型反而容易忽略关键指令。后来精简到只保留核心行为约束把细节放到工具描述里效果反而更好。模型和人一样信息太多会抓不住重点。
返回列表