你有没有遇到过这种情况花了大半天把大模型接进项目结果一问“今天北京天气怎么样”它理直气壮地告诉你一个三天前的预报再问“最新版本的某个依赖库怎么用”它直接给你编出一段并不存在的API。这种事干过一次就明白——大模型所谓的“聪明”建立在它被训练时看到的旧世界上。想让大模型实时获取信息、真正实现自主决策关键不是换一个更大的模型而是改变它与外部世界交互的方式。这篇文章就是写给刚刚上手大模型开发、被“知识截止”和“胡说八道”折磨过的程序员。我会从最基础的问题拆起讲清楚当下主流的三种解决方案RAG检索增强、工具调用Function Calling、以及Agent式的自主决策循环。每种方案我都会给出可以直接抄作业的流程和代码示例也会把我在实际项目中踩过的坑一并交代清楚。无论你是想做一个企业内部知识问答机器人还是想让模型自动完成调研、比价、报表分析这类复杂任务这篇文章都能给你一条清晰可落地的路径。1. 先搞清楚问题大模型为什么“看不到”实时信息1.1 知识截止日期与幻觉问题大模型本质上是一个巨大的概率预测器。它在训练阶段见过海量文本然后把这些文本中的模式压缩成参数。这个过程决定了两件事第一它的知识有“保质期”。训练数据收集完毕的那一刻就是这个模型知识的截止日期。之后世界上发生的事——今天的新冠病毒变异株、某款软件刚发布的版本、你公司昨晚刚更新的内部制度——它统统不知道。第二当它遇到不知道的事情时最自然的反应不是承认“我不知道”而是依据语言模式“编一个最像样的答案”。这就是所谓的幻觉。很多小白程序员以为幻觉是大模型的bug其实它是大模型工作机制的必然副产品。就像一个人被问到从来没学过的问题为了面子硬着头皮答肯定漏洞百出。理解这两点就理解了为什么“实时获取信息”是大模型落地必须跨过的坎。你不可能靠提示词技巧让模型“想起来”它根本没见过的事实唯一的办法是给它一条通往外部真实世界的信息通路。1.2 三条主流路线RAG、工具调用与Agent给大模型接上外部信息目前业界没有花里胡哨的魔法主流的就这三条路第一条RAGRetrieval-Augmented Generation检索增强生成。核心思路是你先准备好知识库用户提问时先去库里把相关的资料搜出来连带着问题一起丢给模型让它“看着资料回答”。相当于考试时允许你翻书你得自己查索引、找到页码、然后把那段内容抄进答卷。第二条工具调用Function Calling。大模型在推理时先决定“这个问题我自己答不了需要调某个外部工具”然后输出一个结构化的调用请求比如我要查天气、我要发邮件、我要算个复杂的数学表达式你的程序拿到这个请求后代为执行再把结果返回给模型由模型整合成最终答案。相当于模型有了手可以自己翻文件柜、打电话问人然后回来继续答题。第三条Agent式自主决策。把RAG和工具调用组合起来再加上循环、记忆和任务拆解让模型可以对一个复杂目标进行多步推理先查数据、再分析、发现缺信息又去查、接着算、最后给结论。这就是所谓的“自主决策”。相当于不仅允许翻书还允许你自己规划先看哪一章、做笔记、算例题最后写出完整解题过程。这三条路不是完全互斥的实际工程中经常配合使用。但作为小白一定要先把每条路的原理和适用场景摸清楚再谈组合。下面我逐条拆解每一步都会配上可复现的实操代码和设计思路。2. 第一个必学方案RAG检索增强让模型“翻书回答”2.1 RAG的核心思路与流程RAG之所以成为大模型落地最普及的方案是因为它解决了“私域知识”的问题。通用模型再强大也读不了你公司内部的制度文件、设备手册、历史故障记录。而RAG可以低成本地把这些资料变成模型可查询的“外挂知识库”而且知识更新只需要换资料完全不用重新训练模型。RAG的标准流程分四步文档切块把长文档分割成合理大小的片段chunk。向量化用文本向量模型把这些片段变成一组数字也就是embedding向量。存入库中把向量连同原文和元数据存进向量数据库建立索引。检索增强用户提问时把问题也变成向量在库里找出语义最相似的几个片段和问题一起交给大模型生成答案。用个生活化的类比向量化就像是给每段资料做了一个“内容指纹”问题来了之后拿“指纹”去库里比对找出内容上最接近的几段原文拼成一份“参考资料小抄”塞给模型。模型拿着小抄回答准确率自然比空口硬答高得多。2.2 实操一个可以在本地跑通的RAG问答机器人下面我用一套开源工具链写一个最小可运行的RAG问答示例。选型说明文本向量化用一个开源的中文向量模型向量数据库用轻量级的本地文件型方案避免一上来就上分布式基础设施。环境准备很简单装两个Python库就行pip install sentence-transformers chromadb然后写一个建库脚本把文档切片、向量化、写入向量库# build_knowledge_base.py from sentence_transformers import SentenceTransformer import chromadb import os # 1. 加载开源中文向量模型 embedder SentenceTransformer(paraphrase-multilingual-MiniLM-L12-v2) # 2. 初始化本地向量数据库 client chromadb.PersistentClient(path./kb_store) collection client.get_or_create_collection(nameproduct_docs) # 3. 切块并向量化 def split_text(text, chunk_size200, overlap50): 简单按字符长度切块带重叠避免切断语义 chunks [] start 0 while start len(text): chunk text[start:start chunk_size] chunks.append(chunk) start chunk_size - overlap return chunks # 假设你已经把文档读成一段长文本 doc_text open(operating_manual.txt, encodingutf-8).read() chunks split_text(doc_text) # 4. 写入集合每条记录带原始文本和元数据 ids [fchunk_{i} for i in range(len(chunks))] metadatas [{source: operating_manual, seq: i} for i in range(len(chunks))] collection.add( idsids, documentschunks, metadatasmetadatas, ) print(f成功写入 {len(chunks)} 个片段)检索和回答部分这样写# query_rag.py from sentence_transformers import SentenceTransformer from openai import OpenAI import chromadb embedder SentenceTransformer(paraphrase-multilingual-MiniLM-L12-v2) client chromadb.PersistentClient(path./kb_store) collection client.get_collection(nameproduct_docs) def retrieve(query, top_k4): 把问题向量化查最相似的片段 results collection.query( query_embeddings[embedder.encode(query).tolist()], n_resultstop_k, ) return results[documents][0] # 拿到片段后交给大模型生成 def generate_answer(query, contexts): context_text \n\n.join(contexts) # 这里接入任意你正在用的模型服务统一走OpenAI兼容接口即可 response client_compat.chat.completions.create( modelyour-llm-model, messages[ {role: system, content: 你是客服助手只能根据参考资料回答资料中没有的内容明确说自己不知道。}, {role: user, content: f参考资料\n{context_text}\n\n问题{query}} ] ) return response.choices[0].message.content if __name__ __main__: q input(请输入问题) contexts retrieve(q) answer generate_answer(q, contexts) print(检索到的片段) for i, ctx in enumerate(contexts): print(f--- 片段{i1} ---) print(ctx[:100]) print(\n最终回答) print(answer)这个示例有几个细节值得注意。第一切块大小和重叠度直接影响检索效果。chunk太小语义不完整chunk太大一个片段里塞了太多无关信息检索精确度下降。我习惯用250到500字左右、重叠50字的参数起步再根据实际文档类型微调。第二向量模型的选择要匹配你的文档语言。中文场景就别只考虑纯英文模型多语言模型或者专门的中文模型效果好得多。第三也是很多小白最容易忽视的检索结果可以看但绝不能直接照抄给用户。一定要让大模型基于检索内容“重新组织语言回答”而且系统提示词里必须明确“资料里没有就承认不知道”否则模型照样会无视资料内容放飞自我。注意有些人图省事直接把用户问题和检索到的所有文本都塞给模型完全没有关联性和去重逻辑。这样检索到的四个片段可能讲的是同一件事模型反而被反复出现的相似内容带偏。建议在拼接前做简单的去重和重排至少按照相关分数排序把最相关的放在最前面。RAG的适应场景非常明确知识相对静态、查询量大、答案需要可追溯引用。比如企业内部的知识库问答、设备维修辅助、法规与合规咨询。但它有一个弱点——它只能“查”既有资料不能执行动作。资料里没写的它就无从下手了。这时就需要第二件武器工具调用。3. 让模型自己“动手”工具调用与结构化输出3.1 Function Calling是什么和RAG有什么区别如果说RAG是让模型翻书找答案那工具调用Function Calling就是让模型具备了“行动能力”。模型在推理时不再仅仅生成最终回答而是可以生成一个“调用某个函数的指令”由你程序执行这个函数得到真实结果再把结果带回给模型。两者的区别可以用一个场景说透用户问“帮我算一下如果项目经费增加15%按目前支出速度还能撑几个月”RAG的做法是去知识库搜“经费使用规则”然后根据文档里的描述“泛泛地回答”一个大概思路。如果文档里没有现成的数字它就只好瞎编。工具调用的做法是模型识别出这是一个需要计算的、且依赖当前最新账目数据的问题于是生成一个调用指令比如calculate_budget_runway(increase_percent0.15)。你的程序执行这个函数去财务系统拉真实的数据算出精确结果再返回给模型。模型基于真实结果组织最终答案。这就是“从自己猜”到“动手查”的本质飞跃。3.2 实操用最少代码实现一个天气查询工具调用下面我用一个获取天气的示例展示Function Calling的核心机制。这里的重点是理解“模型输出结构化调用指令、你的代码执行、再回传结果”这个三角链路。先定义工具。所谓工具就是给模型看的“说明书”和给程序用的“真正函数”两个部分。说明书告诉模型“你有什么工具可以用、参数是什么”真正的函数是Python代码负责干活。import json import urllib.request from openai import OpenAI # 第一步定义工具说明书给大模型看的 tools [ { type: function, function: { name: get_weather, description: 获取指定城市当前天气情况包括温度、降水量和风力, parameters: { type: object, properties: { city: { type: string, description: 城市名称例如北京 } }, required: [city] } } } ] # 第二步定义真正执行的函数给程序用的 def get_weather(city: str) - dict: 调用一个公开天气接口这里用示例端点代替真实服务 url fhttps://api.example.com/v1/weather?city{city} # 真实开发中需要替换成你实际接入的数据源 # 这里直接模拟返回一份结构化数据便于观察流程 mock_data { city: city, temperature: 14.5, unit: celsius, humidity: 65, condition: 多云, updated_at: 2025-01-12T09:30:0008:00 } return mock_data # 第三步定义模型与工具的沟通过程 def chat_with_tool(): client OpenAI(api_keyyour-api-key, base_urlhttps://api.example.com/v1) messages [ {role: user, content: 北京现在的天气适合穿薄外套吗} ] # 第一轮让模型决定是否调用工具 response client.chat.completions.create( modelyour-llm-model, messagesmessages, toolstools, tool_choiceauto, ) assistant_msg response.choices[0].message # 如果模型没有生成调用请求直接返回文字回答 if not assistant_msg.tool_calls: print(模型直接回答, assistant_msg.content) return # 如果模型生成了调用请求逐个执行 for tool_call in assistant_msg.tool_calls: print(模型请求调用, tool_call.function.name) args json.loads(tool_call.function.arguments) result get_weather(**args) print(函数执行结果, result) # 把执行结果作为一条工具消息回传给模型 messages.append(assistant_msg) messages.append({ role: tool, tool_call_id: tool_call.id, content: json.dumps(result, ensure_asciiFalse) }) # 第二轮让模型基于工具结果生成最终回答 final_response client.chat.completions.create( modelyour-llm-model, messagesmessages, toolstools, ) print(最终回答, final_response.choices[0].message.content) if __name__ __main__: chat_with_tool()这段代码跑起来之后你会清楚地看到模型的行为分成两个阶段第一阶段模型判断“关于天气我自己的知识不够新”于是输出一个get_weather调用请求参数是{city:北京}第二阶段你的程序执行完函数拿到实时数据塞回对话里模型再基于数据给出建议。注意几个关键细节工具描述写得越具体模型调用越准确。比如description里写“参数要用中文城市名”还是“要经纬度”直接影响模型填参数的对错。tool_choiceauto的意思是模型自己决定要不要调用工具。它有可能觉得不需要工具直接回答。某些场景下为了强制必须调工具可以设置为tool_choice{type:function,function:{name:get_weather}}但一般不建议强制给模型留出判断空间更好。工具返回的结果是“文本化”的。模型看不到函数内部的执行过程只看到你回传的字符串内容。所以函数的输出要结构化、简明、字段完整按json.dumps序列化回传是最稳妥的方式。3.3 工具调用的设计门道结果截断与错误捕获实际项目中工具返回的数据可能非常大比如查了一份数据库表、调用了一个返回上千条记录的接口。如果直接把海量结果塞回对话会有两个问题一是token费用飙升二是模型被无关信息干扰反而答非所问。我的习惯是所有工具函数统一做三步处理筛选关键字段、限制返回条数、评估是否需要摘要。比如查询订单列表时返回值只保留最新五单的订单号、金额、状态而不是把全量明细灌回去。错误处理也要在工具函数内部消化干净。工具调用失败时处理好返回值和错误信息处理好异常。def get_weather(city: str) - dict: try: # 真实接口调用逻辑 return fetch_from_weather_service(city) except Exception as e: return {error: f获取天气失败: {str(e)}, city: city}这样做的好处是模型发现工具返回了error字段会主动向用户解释“查询暂时失败了请稍后再试”而不是直接卡死流程或者自己编一个天气数据出来。掌握了RAG和工具调用这两个基础能力你已经可以做出相当多实用的应用了。但如果目标更高——“让大模型自主决策”还需要把这两个能力放进一个循环里让模型决定先做什么、后做什么、什么时候收手。接下来就是Agent部分。4. 进阶核心Agent式自主决策循环的构建4.1 从单次调用到多轮推理单次工具调用模型最多做一次“判断-动作-观察”。但真实的任务往往需要一连串动作。比如让大模型“帮我对比三款软件的价格并给出订阅建议”模型需要先搜索A的价格再搜索B的价格再搜索C的价格然后对比最后输出建议。每一步的信息都是下一步的前提。这就是Agent和普通工具调用的区别Agent把“判断-动作-观察”这个过程藏在循环里反复执行直到模型认为任务完成。用学术一点的话说这叫ReAct模式Reasoning Acting。模型每一轮先思考Reasoning“我现在需要什么信息、下一步应该调哪个工具”然后行动Acting调用对应的工具最后观察Observation工具返回了什么、是否满足目标。满足就结束不满足就继续下一轮。你可以把它理解成一个人做菜的过程先看冰箱里有什么查库存工具发现缺了酱油观察于是出去买酱油行动回来再看还缺什么再次观察直到菜能下锅为止。4.2 实操设计一个“自主调研小助手”下面我用一个“多平台产品信息对比”的场景完整展示Agent循环的搭建过程。场景设定用户要求调研某开源数据库在三个平台上的订阅价格和评分输出对比表和建议。注意所有数据源均为示例端点真实使用时替换成你接得上的公开数据或内部服务。先定义Agent需要的工具集查价格、查评分、算总价。然后定义循环主逻辑。import json from openai import OpenAI # 工具集定义 tools [ { type: function, function: { name: query_price, description: 查询指定软件在指定平台上的订阅价格月付美元, parameters: { type: object, properties: { software: {type: string, description: 软件名称}, platform: {type: string, description: 平台名称} }, required: [software, platform] } } }, { type: function, function: { name: query_score, description: 查询指定软件在指定平台上的用户评分满分10分, parameters: { type: object, properties: { software: {type: string, description: 软件名称}, platform: {type: string, description: 平台名称} }, required: [software, platform] } } } ] # 工具执行函数示例实现返回模拟数据 def query_price(software, platform): mock_prices { (OpenDB, 平台A): 29, (OpenDB, 平台B): 35, (OpenDB, 平台C): 31, (FastDB, 平台A): 45, (FastDB, 平台B): 42, (FastDB, 平台C): 50, } price mock_prices.get((software, platform), None) if price is None: return {error: f没有找到{software}在{platform}的价格} return {software: software, platform: platform, price: price} def query_score(software, platform): mock_scores { (OpenDB, 平台A): 8.2, (OpenDB, 平台B): 7.8, (OpenDB, 平台C): 8.0, (FastDB, 平台A): 7.5, (FastDB, 平台B): 8.5, (FastDB, 平台C): 8.1, } score mock_scores.get((software, platform), None) if score is None: return {error: f没有找到{software}在{platform}的评分} return {software: software, platform: platform, score: score} def execute_tool_call(tool_name, args): if tool_name query_price: return query_price(**args) elif tool_name query_score: return query_score(**args) return {error: f未知工具: {tool_name}}接下来是Agent的核心循环。这个循环的关键在于“什么时候结束”。判断依据有两种一是模型自己在回复里明确输出最终答案不再含tool_calls字段二是轮数上限到了。def run_agent(user_query, max_rounds10): client OpenAI(api_keyyour-api-key, base_urlhttps://api.example.com/v1) messages [ {role: system, content: 你是一个调研助手。请围绕用户需求调研数据逐步收集信息最后输出对比表格和结论。每一步只能做一件有用的事。}, {role: user, content: user_query} ] for round_idx in range(max_rounds): print(f\n 第 {round_idx 1} 轮 ) response client.chat.completions.create( modelyour-llm-model, messagesmessages, toolstools, tool_choiceauto, ) assistant_msg response.choices[0].message messages.append(assistant_msg) # 模型没有生成工具调用说明它认为任务已完成 if not assistant_msg.tool_calls: print(Agent判定任务完成输出最终答案) print(assistant_msg.content) return assistant_msg.content # 执行模型请求的所有工具调用 for tool_call in assistant_msg.tool_calls: fn_name tool_call.function.name args json.loads(tool_call.function.arguments) print(f调用工具{fn_name}({args})) result execute_tool_call(fn_name, args) print(f工具返回{result}) messages.append({ role: tool, tool_call_id: tool_call.id, content: json.dumps(result, ensure_asciiFalse) }) # 如果轮数用尽强制终止并提示 if round_idx max_rounds - 1: print(达到最大轮数强制结束) return 任务未在规定轮次内完成请调整查询范围或拆分需求。 if __name__ __main__: run_agent(请帮我调研 OpenDB 和 FastDB 在三家平台上的订阅价格与评分给出对比结论。)这段代码跑起来你会在控制台看到完整循环模型可能第一轮说“我先查OpenDB在平台A的价格”第二轮说“再查OpenDB在平台A的评分”……直到收集完所有字段才会输出最终结论。这个“思考、行动、观察”的痕迹就是Agent的本质。4.3 Agent的安全性设计边界、退出与人工确认Agent看起来很强大但它也是最容易“跑飞”的。我在实际项目里遇到过一个典型场景模型为了查一个简单的价格先去搜索了一堆不相关的行业报告连续调了8个工具token费用烧掉不少最后还没查全。这就是缺少边界约束的后果。给Agent设边界我总结出四个原则第一白名单工具。不要把所有能接的东西都暴露给模型尤其不要开放“执行任意代码”“发邮件”“删除数据”这类高权限工具。默认情况下只开放只读类数据查询工具高权限动作必须由人工审批或在隔离环境执行。第二强制最大轮数。上面代码里的max_rounds不仅仅是防止死循环也是在倒逼模型少做无用功。轮数设置过松模型会“漫游式”探索轮数设置过紧任务又可能完不成。一般调研类任务设8~12轮简单问答设4~6轮比较合适。第三静态工具描述与动态状态提示。工具列表里除了静态定义还要在系统提示词中明确“你已经查到了哪些信息、还差哪些信息”。我给很多Agent加了一个“状态档位”告诉模型“你现在完成了XX下一步应该做XX”。这样模型的行动路径会集中很多不会东一榔头西一棒子。第四敏感步骤的人工确认。凡涉及下单、退款、发送消息等带副作用的动作Agent不能直接执行。正确做法是让Agent生成“待确认操作”并停下由程序弹出确认界面用户点了确认才真正执行。这个机制在工程上叫“人机协同”是Agent落地的关键安全垫。5. 常见问题与避坑实录5.1 问题速查表下面把我做过的项目里最常遇到的几类问题整理成一张表按“问题现象、可能原因、解决思路”三列排列方便你排查时直接对照。问题现象可能原因解决思路模型回答严重依赖检索结果但检索内容不相关切块太大或太小、向量模型和文档语言不匹配、检索top_k设置过小调整切块参数换用合适的中文向量模型调大top_k并观察相关性模型总是调错工具或参数填错工具描述写得太笼统、参数含义不明确、工具命名有歧义把description写细把参数示例写进属性描述工具名避免缩写Agent循环到第五轮开始答非所问上下文里塞了太多工具返回结果模型被噪声干扰控制工具返回长度增加状态摘要把已完成的调研结果单独整理后回传模型明明有工具可以查却直接凭记忆回答系统提示词没有强调“优先用工具”、某些工具可选导致模型懒得调在提示词中说明“你的知识有截止时间涉及实时信息必须调用工具”必要时允许工具调用工具调用失败后模型开始编答案错误结果没有作为正常文本回传模型不知道工具失败工具内捕获异常并返回带error字段的结构化数据让模型“看到”失败原因执行成本居高不下每一轮都把历史工具结果完整塞进上下文token消耗线性增长裁剪历史工具消息只保留当前轮的观察和最终结果摘要模型在最终结论里没有引用检索来源提示词里没有强制要求给出出处在生成模板中要求回答必须标注引用的片段编号增强可追溯性5.2 调试Agent的两个必用技巧调试Agent和调试普通程序完全是两种心态。普通程序出错有堆栈和日志Agent出错往往是因为“模型某一步的推理方向偏了”。我强烈建议在Agent的每一轮循环里都打印完整对话轨迹包括系统提示词、模型思考内容、调用参数、工具返回结果。只有盯着这些中间过程才能真正定位是工具定义的问题、检索质量的问题还是提示词引导的问题。第二个技巧是把“评估”前置。我在接Agent类项目的时候会先准备一组固定的验收问题比如十个典型用户query跑一遍全流程把输出结果存成报告。每次改动系统提示词或工具定义后都用同一组问题回归一遍对比答案质量变化。没有这套评估基线你根本无法判断一次改动是变好了还是变差了。5.3 小白最容易忽略的三个系统工程问题很多人第一次写出能跑的RAG或者Agent demo时都特别兴奋觉得问题解决了。但真实场景会比demo复杂得多我见过太多demo跑通后加几个真实场景就崩掉的情况。这里提醒三个容易被忽略的工程问题第一个是并发与缓存。RAG里向量检索是纯计算密集的Agent的多轮对话调用也是慢操作。如果上线后面向真实用户必须引入缓存相同的问句可以复用之前的检索结果和问答结果降低成本和延迟。我一般会给“问题检索片段集合”算一个哈希值做缓存键这样资料没更新过的重复问题直接就命中缓存。第二个是知识更新的时效性。RAG的知识库不是建完就一劳永逸的。文档更新之后老片段必须同步更新或淘汰。很多团队忘了做这一步导致系统里新旧版本的资料并存检索出来自相矛盾的答案。我的做法是给每个片段打上版本号和生效时间查询和生成时都带上时间过滤。第三个是关于模型选择。不要迷信“越大越好”。在RAG链路里向量模型的选择和生成模型的推理能力是两回事。很多内部知识问答场景一个中等规模的模型配合高质量的检索效果远远好过大模型硬答。大模型有更强的推理能力但也会带来更高的延迟和成本还会让问题“想复杂”反而走偏。我个人实际做下来最大的体会是让大模型实时获取信息、自主决策真正难的从来不是调通一个API而是设计清楚“信息如何进来、模型如何判断、动作如何执行、边界如何守住”。这四个问题想明白了用什么模型、什么框架都是顺手的事。你可以从今天文章里的最小示例开始搭一个只有天气查询工具的Agent跑通流程再逐步增加工具、扩大知识库最终改成你自己的应用。这个从“会回答问题”到“会解决问题”的过程正是大模型开发最迷人、也最锻炼人的一段路。