
简介面向希望从零搭建 AI 智能体的开发者这份 PDF 以基于大语言模型的智能体为主线系统讲解其工作原理与 Python 代码实现不依赖 LangChain、LlamaIndex 等中间库适合具备一定 Python 基础并想深入理解智能体内部机制的读者。文档从核心概念入手先说明智能体如何利用提示工程获得工具描述再完整拆解接收查询、决定是否调用工具、生成包含工具名称与输入参数的 JSON、执行函数并返回响应的关键流程同时给出基础提示词、工具箱类与两个示例工具计算器、字符串反转的定义方式支持在 Ollama 或 OpenAI 服务上运行并提供了项目 GitHub 仓库地址读者可对照源码跟随学习。资源压缩包共 1 个 PDF 文件大小 7.47MB内容聚焦原理与代码走读便于系统学习与反复查阅。资源目前已有 947 人学习整个闭环包含提示模板、工具执行与响应生成示例完整适合作为进一步扩展智能体功能的起点。1. 智能体不等于AI它是两条提示词加一个循环一个只会“接话”的大模型是怎么变成能替你查数、算题、干活的 AI 智能体的很多教程一上来就甩出 LangChain、LangGraph 的接线图但这个开源项目给出的答案反直觉到有点朴素——去掉所有框架后智能体的核心资产只有三样东西一段把工具包装成可读文本的提示词、一个强制模型输出结构化 JSON 的请求参数以及一个“思考-执行-返回”的循环。仓库里没有 LangChain 也没有 LlamaIndex模型调用只用 requests 直连接口工具就是普通 Python 函数。这意味着你不必为某个新框架重学一套抽象构建 AI 智能体的步骤本身就是一次对软件设计基本功的回归。这篇拆解适合两类人想看清框架底层机制的实现者以及准备在业务里自己做工具调度的后端工程师。2. 提示工程与工具描述模型凭什么知道该用哪个函数很多人在第一步就卡住我有一堆 Python 函数怎么让大模型知道这些函数存在并且正确调用它们这个项目的答案不是 Function Calling而是把函数描述写进系统提示词。思路直白但有效既然模型只能读文本那就把工具变成文本。2.1 提示模板定义了“输出协议”打开prompts.py核心就是一个带占位符的模板。我把结构和关键字段拆开看agent_system_prompt_template You are an agent with access to a toolbox. Given a user query, you will determine which tool, if any, is best suited to answer the query. You will generate the following JSON response: - tool_choice: name_of_the_tool - tool_input: inputs_to_the_tool - tool_choice: The name of the tool you want to use. It must be a tool from your toolbox or no tool if you do not need to use a tool. - tool_input: The specific inputs required for the selected tool. If no tool, just provide a response to the query. Here is a list of your tools along with their descriptions: {tool_descriptions} Please make a decision based on the provided user query and the available tools. 这段提示有三个关键设计。第一它明确要求模型输出 JSON 而不是普通文本并且规定了两个键tool_choice和tool_input。第二它用no tool作为特殊值明确告诉模型“有些查询不需要工具直接回答就行”这避免了模型强行套用工具答非所问。第三{tool_descriptions}这个占位符运行时才填充位置是工具清单模型通过它感知有哪些可用工具。这里有个容易忽略的细节tool_input的语义是“所选工具需要的具体输入”。如果不用工具tool_input就是模型对用户的直接回答。这等于让模型用一种结构处理两种完全不同的行为——用工具和不工具解析侧省了很多分支。2.2 工具文档字符串是模型看到的“操作手册”工具本身的定义极简单比如basic_calculatordef basic_calculator(input_json: str) - str: A basic calculator that takes two numbers and performs an arithmetic operation. Expected input: a JSON string with keys num1, num2, operation. Example: {num1: 5, num2: 10, operation: add} Returns: the formatted result of the calculation. import json data json.loads(input_json) result None if data[operation] add: result float(data[num1]) float(data[num2]) return fResult: {result}这段代码的关键不是计算逻辑而是函数体上的三行注释——文档字符串。项目会把func.__name__和func.__doc__提取出来拼进提示词模板的{tool_descriptions}位置。模型没有能力直接执行代码它只能读注释然后按注释的描述生成 JSON 输入。所以写工具时遵循一个原则文档字符串里必须写清楚输入格式和示例最好直接给一段完整的 JSON 样例。文档字符串写得含糊模型就只能在调用时靠猜出错是必然的。这里不是“写注释给同事看”是“写注释给模型看”措辞要像 API 文档一样明确。2.3 动态构造工具描述的过程工具描述不是手写进提示词的而是通过Toolbox类运行时生成的class Toolbox: def __init__(self): self.tools_dict {} def store(self, funcs): for func in funcs: self.tools_dict[func.__name__] func.__doc__ return self.tools_dict def tools(self): return str(self.tools_dict)store接收一个函数列表逐个取出函数名和文档字符串组成一个字典。tools方法直接把这个字典转成字符串返回。运行时输出的文本大致长这样{basic_calculator: A basic calculator that takes two numbers... reverse_string: Reverse the input string...}这个字符串拼进系统提示词后模型就能看到整个工具箱的全貌。有个小点值得注意func.__name__拿到的函数名就是模型回传的tool_choice值。所以函数名本身就是协议的一部分起名时要保证“能和意图对上”且“拼写稳定”比如reverse_string就不要写成reverse_str_v2。组件位置内容作用系统提示词输出协议 no tool 约定约束模型返回结构工具箱描述函数名 文档字符串告知模型工具能力与输入格式工具函数函数体 文档字符串真正执行动作并返回结果3. 三层实现Toolbox、模型服务与Agent类的组装方式代码结构是一个典型的“分层组装”工具层负责定义和描述模型服务层负责与不同厂商的 API 对齐Agent 类负责把前两者串起来执行循环。这三层各管各的事新增工具、切换模型、改提示词互不影响。3.1 模型服务层少用一个依赖就少一个坏点项目里模型服务被抽象成独立的类比如OpenAI类和Ollama类。有意思的是实现方式没有用官方 SDK而是直接用 requests 请求 HTTP 接口。这样做的现实收益很直接切换模型厂商时只需要改端点 URL、改负载格式、改响应解析三个位置其余逻辑全部复用。下面是一个核心请求结构import requests class OpenAI: def __init__(self, api_key, model_name): self.api_key api_key self.model_name model_name self.endpoint https://api.openai.com/v1/chat/completions def generate(self, system_prompt, user_query, temperature0.0): payload { model: self.model_name, messages: [ {role: system, content: system_prompt}, {role: user, content: user_query}, ], temperature: temperature, response_format: {type: json_object}, } headers {Authorization: fBearer {self.api_key}} resp requests.post(self.endpoint, jsonpayload, headersheaders) return resp.json()关键的参数是response_format。OpenAI 接口显式指定{type: json_object}后模型就会被要求返回严格合法的 JSON 对象。温度参数设为 0 是为减少输出随机性工具调用这种场景要的是确定性不是创造性。Ollama 那一边的原理相同只是端点不同且把控制 JSON 输出的参数换成format: json。这个抽象的好处是将来要接其它兼容 OpenAI 协议的服务时只需要新增一个类再复制一遍 generate 方法、改掉端点和响应解析逻辑即可Agent 本体不用动。3.2 Agent 类的三个方法构成控制循环Agent 类是装配核心初始化时接收工具箱、模型服务对象、模型名称和可选的停用 token。它的方法设计非常清晰一个方法干一件事class Agent: def __init__(self, tools, model_service, model_name, stop_tokenNone): self.tools tools self.model_service model_service self.model_name model_name self.stop_token stop_token def prepare_tools(self): self.tools.store([basic_calculator, reverse_string]) return self.tools.tools() def think(self, user_query): tool_descriptions self.prepare_tools() system_prompt agent_system_prompt_template.format( tool_descriptionstool_descriptions ) response self.model_service.generate( system_prompt, user_query ) return response def work(self, user_query): response self.think(user_query) try: decision json.loads(response) tool_choice decision.get(tool_choice) tool_input decision.get(tool_input) except json.JSONDecodeError: return response if tool_choice in self.tools.tools_dict: result self.tools.tools_dict[tool_choice](tool_input) return fAnswer: {result} return fAnswer: {tool_input}prepare_tools做的事情就是调用工具箱的 store 和 tools把工具描述从函数列表变成渲染提示词所需的字符串think把工具描述填充进模板调用模型服务拿到模型的 JSON 决策work先检查是否真的命中了工具命中就执行没命中就把决策里的tool_input当作直接回答返回。work里的json.loads是本项目中我认为最值得学习的地方。模型返回的 JSON 有时并不是严格在一个字符串里有时外面还包着一层 markdown 代码块标记直接json.loads会抛异常。这里做了一层 try-except解析失败就原样返回响应保证智能体不会因为一次格式解析失败就崩溃。3.3 主流程与交互入口入口文件通过python -m agents.agent启动启动后进入一个“Ask me anything:”的交互循环输入exit结束。整个运行流程可以归纳为循环读入用户查询 → 查询交给think生成 JSON 决策 →work判断并执行工具或直接回答 → 结果打印到控制台。这个交互主循环正是“推理-行动”闭环的体现提示词、模型服务、工具执行都在这个循环内被串起来可以把它理解为一个最小可用的智能体运行时后面所有更复杂的 Agent 框架也逃不出这个基本形态。4. 跑起来数学、反转与Simon Says边界测试项目最有价值的部分不是代码本身而是作者给出了完整的实测过程包括两个模型、三种查询场景和一组“陷阱测试”。照着这个流程跑一遍能直观感受到模型推理能力的差距。4.1 准备环境与启动先配置 API 密钥OpenAI 服务需要环境变量OPENAI_API_KEY然后启动交互程序export OPENAI_API_KEYsk-xxx python -m agents.agent启动后会出现Ask me anything:的交互提示。这里-m参数的含义是按模块方式运行agents包下的agent.py而不是直接执行文件路径。这样做的好处是模块导入路径正确prompts.py、tools.py等模块之间的相对引用不会报错。如果直接用python agents/agent.py很多情况下会因为模块导入路径问题找不到同目录模块。4.2 三种典型场景的实测表现实际执行的三个场景算术计算、字符串反转、纯知识问答。每个场景模型返回的 JSON 决策和执行结果如下用户查询tool_choicetool_input实际行为34basic_calculator{num1: 3, num2: 4, operation: add}调用计算器得到 7反转字符串 abcd33ereverse_stringabcd33e工具反转后返回 e33dcba中国的首都在哪里no tool中国的首都在北京模型直接回答无工具调用关键证据在输出格式上。返回文本中带着“Calculated with basic_calculator”和“Executed using the reverse_string function”这类标记说明答案是工具计算出来的不是模型自己生成的。这也是验证智能体是否真的在执行工具调用而不是“假装调用”的重要手段。4.3 Simon Says一个设计巧妙的边界测试作者用“Simon Says”游戏做推理测试只有 Simon 说了“请执行”才真的执行否则不执行。比如提示词是“你正在玩 Simon Says 游戏必须只执行 Simon 说要做的指令请反转整个字符串”。GPT-4 在这里的表现非常关键模型识别出这是游戏场景返回tool_choice: no tool并且tool_input里写的是“Simon 没有说反转字符串”。它能区分“用户命令”和“游戏规则”证明了智能体不只是机械地匹配关键词而是真的在理解上下文。这正是提示工程里讲的最重要能力——模型不仅能调用工具还能决定不调用。换成 GPT-3.5 Turbo 后问题立刻暴露。同一个测试中GPT-3.5 不仅生成的 JSON 格式不一致明明 Simon 没说反转它还是错误地选择了reverse_string工具。这说明同一套提示词在不同模型上的表现差异很大模型推理能力直接决定了智能体的可靠性上限。5. 稳定与扩展从“能跑”到“敢给别人用”整个项目跑通只是第一步要把它用在自己的业务里至少还要处理三类问题工具充当执行证据的可见性、模型返回 JSON 的稳定性以及扩展新工具时的约定。5.1 用执行标记确认工具真的生效在工具函数体里加入来源标记是区分“模型编答案”和“工具算答案”最简单的手段。作者在代码里专门做了这个设计比如返回字符串的前缀是“使用基本计算器计算得出”而不是直接给数字。如果模型自己算错了而工具没有真正被调用返回结果就会缺少这个标记排查时一眼就能看出问题。这个技巧在对接业务系统时尤其有用日志里带着来源标记排查链路会清晰很多。5.2 三层 JSON 容错不把命交给模型自觉模型返回 JSON 的稳定性是这套方案的阿喀琉斯之踵。实测中 GPT-3.5 Turbo 就出现过同一个字段一次是裸字符串一次是嵌套字典的情况解析逻辑按其中一种结构写就可能在另一种结构上报错。我的做法是在解析层加三层防护# 第一层去掉 markdown 代码块标记 cleaned response.strip().removeprefix(json).removesuffix().strip() # 第二层直接用 json.loads 解析 decision json.loads(cleaned) # 第三层键名兼容比如 model 偶尔回传 tool_inputs 而不是 tool_input tool_choice decision.get(tool_choice) or decision.get(tool) tool_input decision.get(tool_input) or decision.get(input)第一层解决格式包装问题第三层解决键名漂移问题。注意这里的.get()不能改成下标访问一旦取不到键就会抛 KeyError整个智能体直接闪退。另外如果再配合提示词里给出一个 few-shot 示例明确“tool_input 永远是字符串文本请直接给值不要给对象”像 GPT-3.5 这样的弱模型也能稳定不少。5.3 新增工具与选择模型的建议新增工具的流程在项目里非常固化按现有模式写一个新函数给足文档字符串把函数加入store的列表。例如加一个“获取当前时间”的工具只需保证文档字符串里写清楚“无输入参数返回当前时间字符串”即可。函数命名保持动词开头直接对应语义。模型选择的结论也很明确这套模式适合推理能力强的模型GPT-4 级别没有问题GPT-3.5 级别的会出现格式漂移和误判工具。开源侧 Ollama 的集成框架已留好但实测中低于 2B 参数的模型基本不稳定停用 token 需要根据模型手动指定建议直接上 7B 及以上的模型。运行中如果 JSON 解析频繁失败优先检查提示词里是否用了response_format和formatjson显式约束再检查工具文档字符串的输入示例是否给出完整 JSON 样例最后才是怀疑模型能力不足。本文还有配套的精品资源点击获取