简介:面向希望快速上手Agent系统开发的开发者,这份资源提供了一套完整的可运行源码,覆盖从基础概念到落地实现的关键环节。源码实现了Researcher、Editor、Note Taker三个角色的分工协作,并集成搜索工具与笔记工具,可支撑AI搜索、报告生成和自动笔记等应用场景,同时演示了从离线笔记系统升级为联机增强版的实际路径。资源共9个文件,以4个Python脚本为核心,另含依赖清单、环境配置示例与说明文档,压缩包仅15KB,结构简洁、方便二次开发。目前已有148人学习下载,适合具备一定Python基础、希望参考完整Agent实现并理解RAG应用逻辑的开发者,可以作为自主搭建或扩展Agent系统的起点。
搭建Agent系统指南:一份能直接跑起来的源码
我见过太多人搜“Agent搭建教程”,结果翻到的全是概念轰炸——什么ReAct、什么多智能体协作、什么记忆分层,讲得天花乱坠,但你想跟着动手的时候,连一个能python main.py跑通的项目都没有。这感觉太糟糕了。
今天我不讲虚的。我把自己实际维护的一套Agent系统最小可运行版本完整拆给你看,附可运行源码。这套代码核心逻辑只有几百行,没有用任何重型框架,底层就是标准的 LLM API 调用 + 工具注册机制 + 记忆管理 + 主循环。但它是一个真正具备“思考—行动—观察”闭环的Agent,不是玩具。
它适合三类人:刚入门LLM开发、想在真实项目里落地Agent的新手;已经会用LangChain之类的框架、但想搞明白底层原理的开发者;还有正在准备Agent方向面试、需要一张“全流程图”的同学。看完你会知道,一个Agent系统最核心的不是模型,而是围绕模型搭建的这套执行框架。
1. 项目整体思路与架构设计
1.1 核心需求解析:什么是“能跑起来”的Agent
先给一个我在项目里验证过多次的定义:Agent = LLM(大脑)+ 工具(手脚)+ 记忆(经验)+ 控制循环(神经反射)。
一个纯粹的聊天机器人不是Agent,因为它只能“说”,不能“做”。而一个能自己决定调用哪个API、填写什么参数、看到返回结果后再决定下一步动作的系统,才是真正的Agent。
这里面最关键的转折点是:Agent把“决策权”交给了模型。传统程序的控制流是开发者写死,if-else一层套一层;Agent的控制流是模型根据当前任务上下文动态生成的。这也是为什么Agent天然适合那些“流程没法提前穷举、但目标明确”的场景。
所以,搭建Agent系统第一步不是选框架,而是设计四个模块的边界:
- LLM接口层:负责和模型对话,统一处理system/user/assistant消息格式。
- 工具执行层:把外部能力(查天气、发邮件、算算术、查数据库)封装成带描述的函数,暴露给LLM。
- 记忆管理层:保存对话历史、上下文、关键中间结果。
- Agent主循环:把前三者串起来——模型决定调用哪个工具,执行工具,把结果写回上下文,模型继续判断下一步,直到任务完成。
1.2 源码目录结构(最小可用版)
我手头这个版本就是按上面四个模块划分的,目录非常清爽:
agent-demo/ ├── main.py # 入口文件:初始化Agent并对话 ├── agent_core.py # Agent主循环与控制逻辑 ├── provider.py # LLM接口封装(兼容OpenAI格式) ├── tools.py # 工具注册与工具定义 ├── memory.py # 上下文记忆管理 ├── config.yaml # 模型、温度、步数等配置 ├── requirements.txt # 依赖清单 └── logs/ # 运行日志目录这个结构是我刻意裁过的。真在业务里做Agent,你至少要再拆出tool_registry模块、独立的storage组件、以及更细的任务规划层。但对于一个刚从零搭建、需要先跑通闭环的工程来说,这个结构恰好能让你看清每一行代码在整条链路里的位置。
1.3 核心设计决策:为什么不用现成框架
我经常被人问:现在LangChain、AutoGen、MetaGPT这么成熟,直接拿来用不香吗?
香,但不适合每个人。我见过团队接入LangChain之后,报错的时候完全不知道在哪一层出了问题,函数调用链绕得人头大。框架帮你解决了80%的通用问题,同时也把20%的定制空间和所有排查成本一起包了进来。
我在这套代码里的立场是:核心机制手写,外围可以接框架。你完全可以在跑通这套源码之后,把它的主循环整体替换成LangGraph的StateGraph,或者把其中某个工具换成LangChain自带的Tool。了解底层之后,你用任何框架都会比死记硬背文档的人有底气得多。
2. 核心源码模块逐行拆解
2.1 LLM接口层:OpenAI兼容格式的最小封装
先看provider.py。现在国内很多模型服务都兼容OpenAI的接口风格,所以我默认用OpenAISDK 风格来写,这样可以一键切换base_url指向本地模型(比如用 Ollama 或 vLLM 开的服务)。
# provider.py from openai import OpenAI class LLMClient: def __init__(self, api_key: str, base_url: str, model: str): self.client = OpenAI(api_key=api_key, base_url=base_url) self.model = model def chat(self, messages: list, temperature: float = 0.3) -> str: resp = self.client.chat.completions.create( model=self.model, messages=messages, temperature=temperature, ) return resp.choices[0].message.content看起来简单,但这里埋着一个关键设计:外层传进来的messages必须是完整的历史消息列表,而不是单轮对话。因为LLM本身是无状态的,Agent系统所谓的“记忆”,本质是把以往交互的上下文拼到每次请求里。
如果你只是接API聊天,这一步就够了。但要让模型稳定输出“工具调用指令”,你需要在 prompt 里明确告诉模型:“当你需要外部能力时,输出一段JSON,格式为 {'tool': '工具名', 'args': {...}}。”具体写法我在第四节说。
2.2 工具注册机制:Agent能做什么,全看这里
这一块是整个系统里灵魂层面的东西。Agent执行能力的来源就是工具集合。我在tools.py里用一个装饰器维护工具注册表:
# tools.py import json, requests TOOL_REGISTRY = {} def register_tool(name: str, description: str): def decorator(func): TOOL_REGISTRY[name] = { "function": func, "description": description, "name": name, } return func return decorator @register_tool( name="get_weather", description="查询指定城市的实时天气,输入城市名,例如:北京", ) def get_weather(city: str) -> str: url = f"https://api.openweathermap.org/data/2.5/weather?q={city}&appid=YOUR_KEY&lang=zh_cn" resp = requests.get(url) data = resp.json() if data.get("weather"): return f"{city}天气:{data['weather'][0]['description']},温度{round(data['main']['temp']-273.15, 1)}℃" return f"没有查到{city}的天气" @register_tool( name="calculator", description="进行加减乘除四则运算,输入形如 '12 * 8 + 5'", ) def calculator(expression: str) -> str: try: return str(eval(expression)) except Exception as e: return f"计算错误:{str(e)}"有人会问,为什么做注册表而不直接在代码里if tool_name == "get_weather": ...?区别在于:注册表是数据驱动,后续加新工具只需要新增一个函数加个装饰器就行,主循环一行都不用改。而且注册表本身可以遍历,把工具描述全部塞进system prompt,让模型“知道”Agent有哪些能力。
注意:工具描述写得是否清楚,直接决定模型选不选对工具。你在真实项目里写工具描述时,要写“什么时候用这个工具”和“参数要求”,不要只写一句“查天气”。
2.3 记忆模块:先跑通,再谈复杂记忆
很多教程一上来就讲向量数据库、长期记忆、短期记忆,但对于一个刚启动的项目太重了。我第一版记忆就做了两件事:保存消息历史和保留最大上下文条数。
# memory.py class Memory: def __init__(self, max_turns: int = 10): self.history = [] self.max_turns = max_turns def add_user_message(self, text: str): self.history.append({"role": "user", "content": text}) def add_assistant_message(self, text: str): self.history.append({"role": "assistant", "content": text}) def add_system_message(self, text: str): self.history.insert(0, {"role": "system", "content": text}) def get_context(self) -> list: # 只保留最近max_turns轮,避免上下文窗口爆掉 return self.history[-(self.max_turns * 2 + 1):]为什么只保留最近十轮?因为LLM的上下文窗口有限,而且塞入大量陈旧信息既浪费token,还可能干扰模型对当前意图的判断。你可以把这里的max_turns理解成“短期工作记忆”。
等你自己跑通之后再去做升级:把每轮关键信息抽成摘要存进summary_memory,或者把历史知识按向量存储,每次根据当前问题检索Top-K相关片段。但那是后话,第一步先用滑动窗口跑起来。
2.4 Agent主循环:思考—行动—观察
最核心的就是agent_core.py里的循环函数。因为模型返回的不是标准函数调用格式,所以我这里用一个中间协议:让模型在需要工具时,严格输出一个可解析的JSON块,然后系统用json.loads解析、执行工具、把结果返回给模型。
# agent_core.py import json from tools import TOOL_REGISTRY SYSTEM_PROMPT = """你是一个智能助手。 你可以使用的工具如下: {TOOL_LIST} 当用户的问题需要外部能力时,请严格输出如下JSON格式(不要输出其他内容): {{"tool": "工具名", "args": {{"参数名": "参数值"}}}} 如果你认为任务已经完成,直接回答用户即可,不要输出JSON。""" def run_agent(user_input: str) -> str: memory.add_user_message(user_input) tool_used_count = 0 max_tool_calls = 5 while True: context = memory.get_context() response = llm.chat(context) # 尝试解析JSON try: action = json.loads(response) tool_name = action.get("tool") args = action.get("args", {}) except json.JSONDecodeError: # 模型没输出JSON,说明在直接回答 memory.add_assistant_message(response) return response if tool_name not in TOOL_REGISTRY: memory.add_assistant_message(f"工具 {tool_name} 不存在,请重新选择可用工具") continue # 执行工具 tool_func = TOOL_REGISTRY[tool_name]["function"] observation = tool_func(**args) memory.add_assistant_message(f"调用工具{tool_name},参数{args},结果:{observation}") tool_used_count += 1 if tool_used_count >= max_tool_calls: return "已达最大工具调用次数,任务终止"这里尤其要注意几个现实问题:
json.loads大概率会失败。模型经常会在JSON外面包一层 ```json 标记,或者前面加一句“好的!我来查询:”。解决办法我在第四节给出。- 无限循环必须限制。加
max_tool_calls是最基本的兜底,否则一个错误的工具描述就能让Agent一直空转。 - 工具执行结果本身就是“观察”。ReAct 范式里的“观察”不是模型自己推理出来的,而是真实工具返回的数据。你写回去的消息要干净、可读,方便模型下一步做判断。
3. 完整部署实操:从零到一的跑通路径
3.1 环境准备与配置
先创建一个虚拟环境,然后安装依赖:
python -m venv .venv source .venv/bin/activate # Windows 用 .venv\Scripts\activate pip install openai pyyaml requests在config.yaml里集中管理配置:
# config.yaml llm: api_key: "sk-xxxx" base_url: "https://api.openai.com/v1" model: "gpt-4o-mini" temperature: 0.3 agent: max_tool_calls: 5 max_turns: 10把配置文件和代码解耦,是为了避免你以后换模型、换参数的时候到处改代码。我就是因为在真实项目里被“裸配置”坑过一次,现在养成习惯:任何模型名称、API地址、循环次数,全部走配置。
3.2 main.py:组装整个系统
main.py做的事情就三件:读配置、初始化各个模块、启动一个简单CLI对话。
# main.py import yaml from provider import LLMClient from agent_core import run_agent with open("config.yaml", "r", encoding="utf-8") as f: config = yaml.safe_load(f) llm = LLMClient(**config["llm"]) def main(): print("Agent已启动,输入exit退出") while True: user_input = input(">>> ") if user_input.lower() in ("exit", "quit"): break result = run_agent(user_input) print(f"\nAgent: {result}\n") if __name__ == "__main__": main()agent_core.py里的run_agent引用的是模块级变量memory和llm。我这里为了演示简化了,真实项目里你应该用类或依赖注入的方式,把这些依赖显式传进去。原因你自己跑两次就明白了:全局变量在多人协作和复杂测试里会变得非常难维护。
3.3 实际运行:一个完整对话过程拆解
我把这套代码跑起来之后,测试了一个典型请求:“我明天要去杭州,帮我看看那边的天气,顺便算一下如果气温30度,换算成华氏度是多少。”
模型第一轮返回的不是最终答案,而是工具调用指令:
{"tool": "get_weather", "args": {"city": "杭州"}}系统执行工具,把结果追加进上下文,再次请求模型。模型看到天气结果之后,又输出第二条工具调用:
{"tool": "calculator", "args": {"expression": "30 * 9 / 5 + 32"}}第二次工具返回结果后,模型才产出最终回答:“杭州明天阴转小雨,23℃,30℃换算成华氏度为86℉。”
注意看这个过程:Agent不是一步到位的,它是根据每次观察动态规划下一步动作。这正是Agent和普通程序的分水岭。你不需要把所有分支提前写死,只要给模型足够的工具和清晰的边界,它会在运行时自己设计执行路径。
4. 常见问题与排查技巧实录
做Agent调试是最磨人的,因为输出的不确定性意味着你今天能跑通的流程,明天换几个字就翻车。我把自己踩过的坑整理成一张速查表:
| 问题现象 | 根因 | 解决方案 |
|---|---|---|
json.loads频繁报错 | 模型在JSON里夹带了 ```json标记 | 加载前先去除代码块标记,找第一个{和最后一个}做截取 |
| 模型总是不调工具 | 工具描述不够明确,或system prompt没强调规则 | 在prompt里明确“需要外部能力时,必须输出JSON,否则任务无法完成” |
| Agent陷入死循环 | 工具结果触发模型不断调用同个工具 | 加最大步数限制;或在prompt中要求“如果工具返回异常,直接向用户说明情况” |
| 上下文迅速膨胀 | 工具调用结果被完整塞入历史,多轮后token爆炸 | 对工具结果做截断,或只保留最近几轮上下文 |
| 温度太高导致工具指令不稳定 | 默认温度0.7太高 | 涉及工具调用时,建议temperature控制在0.2~0.3 |
4.1 JSON解析失败的高效兜底
我见过很多Agent项目挂在“模型不按格式输出”这一关。一个稳妥做法是:解析失败时,不是直接返回错误,而是把“刚才的输出无法解析”作为系统消息追加回上下文,让模型自己修正。这一步相当于把“犯错—修正”也做成了一次思考回合。
4.2 工具执行错误别急着抛异常
当工具执行报错时,把异常信息以观察结果的形式写回上下文,比直接让程序崩溃要好。模型看到 “输入参数格式不对,应该传城市名称” 这样的反馈,往往可以自己修正后再调一次。这种容错方式一开始看起来不“严谨”,但在LLM应用里是常态。你的Agent不是在跟确定代码打交道,而是在跟概率模型博弈。
4.3 记录完整运行轨迹
强烈建议在每次工具调用前后加上日志输出:
[2025-01-01 10:00:01] 模型输出: {"tool": "get_weather", "args": {"city": "杭州"}} [2025-01-01 10:00:03] 执行工具 get_weather,返回: 杭州小雨,22℃日志是Agent调试的生命线。因为模型行为不可完全复现,所以没有日志你根本没法定位是“模型决策错误”还是“工具执行错误”。有同事跟我抱怨Agent效果不行,我上去第一件事就是翻日志,结果发现是工具入参错了——模型传了空字符串,工具直接报错。
最后分享两个实操心得
第一,Agent的很多问题不是“模型不够聪明”,而是上下文信息不够完整或工具描述不够清楚。我调试过不下十个Agent项目,结论惊人的一致:在Prompt和工具描述上花力气,比盲目换更强力的模型更有效。
第二,如果你要把这套代码往生产方向引,优先补三个能力:结构化输出校验(比如用Function Calling或者Pydantic做数据校验)、任务队列与进度管理(异步长任务必备)、以及多轮任务下的全局规划。现阶段这个源码是最好的起点——它足够小,能让你彻底掌握Agent的运作机制;它也足够稳,我本地连续跑了一周没有崩过。
把这段代码拷贝下来,改一行模型名,填上你的API Key,就可以开始你第一个Agent的实验了。别怕踩坑,那些坑都是你真正理解Agent的开始。
本文还有配套的精品资源,点击获取