ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

手把手构建你的第一个AI Agent:记忆、角色与主动技能全实现

手把手构建你的第一个AI Agent:记忆、角色与主动技能全实现 我最早被自己写的 Agent 惊艳到是发现它可以记得三分钟之前自己说过什么并且像个有脾气的人一样反问了我一句“你确定要改成这个方案上次你让我改完以后又改回去了。”那一瞬间我才意识到真正值得写进代码里的东西不只是“能调 API”而是让程序拥有一套稳定的记忆结构、一个清晰的边界角色、一种遇事能主动判断下一步的行为模式。这就是这篇博文想带你实现的东西手把手从零写出你的第一个 AI Agent并且给这个 Agent 挂上真正有用的 Skill让 Skill 不再只是一个被动触发的函数而是自带上下文、见过世面、会主动做事的逻辑单元。这篇文章适合什么人来读适合那些已经会写 Python、能调通大模型接口但一直被以下问题卡住的人什么是 Agent 的核心区别Skill 到底装的是什么东西Agent 拆成角色、记忆、主动性之后每一层怎么落地成代码。文章里不会出现说教式的概念堆叠我会直接给一个最小可运行骨架然后一行行拆解设计意图最后分享一些我踩过的坑和一套可以自己验证 Agent 写得好不好的小方法。读完以后你至少能写出一个属于自己的、具备角色边界和历史记忆的 Demo Agent并掌握把任意新能力封装进 Skill 的方法。1. Agent 与 Skill 整体设计与思路拆解1.1 先搞清楚 Skill 和 Agent 到底什么关系很多文章把 Agent 和 Skill 混在一起讲这两个概念在 2026 年这个时间点上已经有必要做一个清晰的区分。我的理解是Agent 是一套能够自主完成“感知—规划—行动—反思”闭环的运行框架Skill 是这套框架内部可以被复用的能力单元它既承载了某类任务的执行指令也可以携带这段任务相关的历史经验与知识片段。用一个做菜类比。Agent 是一整套厨房管理系统包括灶台、水槽、流程和调度。Skill 则是一份“麻婆豆腐标准化操作卡”里面写清楚需要什么食材、火候多少、什么时候翻锅。如果只是把操作卡贴在墙上它不会主动帮你烧菜这在传统编程里叫函数库。但如果你把操作卡交给一个能自己看火、自己翻锅、还会根据上次做咸了调整盐量的系统操作卡才真正变成了 Skill。所以 Skill 和 Agent 之间有一个明显的层次结构Agent 是载体和执行主体Skill 是装载在主体身上的一种可调用的、带语境的技能插件。在 Agent 系统里一个 Skill 往往由三部分组成。第一部分是描述文件告诉 Agent 这个 Skill 是干什么的、在什么情况下应该被调用、需要哪些参数第二部分是执行逻辑是真的一段可运行的代码或者是一套非常详细的提示词指令第三部分是记忆空间用来保存这个 Skill 在多次调用过程中积累下来的有效信息和修正经验。可以这样记忆Skill 是 Agent 身上可插拔的执行模块拥有记忆和场景适配能力的 Skill 才是真正的高级技能而没有记忆的 Skill本质上等同于普通函数调用的语法糖。1.2 标题里三个关键词的代码含义“记忆、角色、主动性”不是包装出来的概念它们分别对应 Agent 架构里三个必须单独设计的组件。记忆在代码层面拆成两层。第一层是短期工作记忆通常就是多轮对话历史里有限数量的消息记录LLM 模型的大上下文窗口其实就能充当短期记忆的载体。第二层是长期记忆需要把有保留价值的信息抽取出来写入本地 JSON 文件、SQLite、向量数据库等专门做持久化的地方。人不可能记住每一次对话的每个字但一定会记住关键结论和偏好Agent 的长期记忆做的是同样的事。角色更多不是玩法而是约束。代码层面的角色可以通过 System Prompt 注入系统设定也可以用一个专门存放角色描述的 Profile 对象统一管理。角色设定的意义在于维持响应边界、明确行为范围和形成稳定风格它不应该散落在每一轮请求里而应该以可配置、可变更的形式独立存在这是为了让 Agent 的所有行为都由一套明确的中心化人格定义控制。主动性体现在 Agent 的执行循环里。一个真正主动的 Agent 不会像聊天机器人那样“你说一句、它答一句”而是拿到任务后会先拆解再判断是否要调用 Skill、是否要查询某个信息、是否需要多步执行。传统的函数调用是最早的主动意识但完整的主动性长这样持续分析当前状态选择下一步最佳动作做完一个动作以后观察结果再重新决策直到整个问题闭环。1.3 核心方案一个最小 Agent Skill 管理器的骨架设计为了不让骨架复杂到没法写我建议这样设计第一版架构一个核心 Agent 类负责主循环和模型请求一个 Skill 基类负责定义接口另外给 Agent 配一个 SkillRegistry 组件用来登记和管理它当前拥有的全部 Skill。Agent 类的核心循环只有一个 while 结构。给大模型发送当前消息和历史记忆之后LLM 返回的结果中如果带有特殊标记说明需要调用某类工具就把这段解析出来交给 SkillRegistry 调度执行把执行结果重新塞回上下文再交给大模型判断下一步如果大模型没有给出调用标记说明它认为任务已经完成循环结束。整个循环的关键在于“结果反馈”和“迭代执行”这是 Agent 和普通 Chat 接口之间最显著的差异。SkillRegistry 则是插槽管理系统。它要维护一张表表里记录技能名称、技能功能描述、参数 schema、调用地址。Agent 每轮主循环开始前会把注册表里的技能摘要信息填入系统提示词让大模型知道自己有哪些工具可用。这里我踩过的第一个坑是如果把完整的技能源码全部塞进上下文很快会爆 token所以描述信息要精简到一句话加三五个关键参数同时把完整代码留在 Skill 对象内部。1.4 技术选型为什么用 Open AI 兼容接口 JSON 描述协议2026 年大模型 API 生态已经比较成熟不同厂商提供的函数调用格式虽然略有差异但基本都是围绕 JSON 结构和工具描述协议展开。为了让自己写的 Agent 不被锁死在一家平台上我选择了任何支持 OpenAI 兼容 /chat/completions 接口的模型服务作为底座这样将来换供应商时成本最低。Skill 对外暴露的参数协议统一采用 JSON Schema 格式。这个选择有两个原因一是 JSON Schema 本身是模型厂商工具调用最通行的标准直接复用不用自己做方言翻译二是 JSON Schema 的描述能力足够表达必填项、可选值、类型和约束能有效提高模型解析参数的成功率。示例技能里我会定义一种“read_skills_dataset”协议它传入文件路径、解析方式、返回批量几个参数注册表拿到参数以后才真正执行文件读取逻辑。因为要支持 Agent 调用本机能力和读取自己的技能库第一版不引入重型服务框架。你只需要保证自己在本地可以执行 Python3.10同时本机能访问到部署好模型服务的地址最好再准备一个 .env 文件存放 API Key。我不建议第一版就上 LangChain 之类的大而全框架先徒手把最原始的循环写明白再横向比较框架才是更高效的学习路线。2. 从零搭建可运行骨架核心代码模型2.1 环境准备与目录结构开始之前你需要一个干净的目录结构。我建议按照下面的布局组织工程文件这既方便以后扩展新技能也方便测试。myagent/ ├── .env.example ├── agent_core.py # Agent 主体逻辑 ├── skill_base.py # Skill 基类 ├── skill_registry.py # 技能注册表 ├── skills/ │ ├── __init__.py │ ├── notes.py # 示例技能笔记记忆 │ └── web_looker.py # 示例技能联网查询可替换 ├── memory/ │ └── agent_store.json # 长期记忆持久化文件 └── demo_run.py # 启动脚本版本依赖方面尽量克制只需要 openai、python-dotenv、pydantic。openai 这个包虽然名字看起来是某家公司的 SDK但它已经是一个事实上的通用客户端库支持配置 base_url 指向任意兼容接口的服务。安装依赖和执行环境用以下命令python3 -m venv venv source venv/bin/activate pip install openai pydantic python-dotenv requests cp .env.example .env注意 .env.example 里至少要放三样你的模型服务地址 LLM_BASE_URL、模型名称 LLM_MODEL_NAME、密钥 KEY_NAME。我不写死是哪个厂商因为你的部署环境要以实际能访问的服务为准。2.2 为 Skill 定义通用接口把记忆装进类属性Skill 这个对象不应该只是“一个函数名”而是要具备任务描述、参数说明、执行函数、记忆操作方法四个基本特征。我可以定义一个偏向于契约式的基类# skill_base.py from typing import Any, Callable, Optional class Skill: name: str unnamed description: str parameters: dict {} # JSON Schema memory: dict {} def __init__(self, memory_store: Optional[dict] None): if memory_store: self.memory memory_store def execute(self, **kwargs) - Any: raise NotImplementedError def remember(self, key: str, value: Any): self.memory[key] value def recall(self, key: str, defaultNone): return self.memory.get(key, default)我解释一下设计意图。execute 是核心动作入口所有调用者只关心给它参数返回结果。remember 和 recall 是注入长期记忆的简单接口记忆可以存储在外部字典里后续可以替换为真实的 JSON 文件层。这套写法最直接的好处就是你不需要理解插件系统的高深概念就能在一个 Skill 实例内同时维护“怎么做”和“我记得什么”两件事。2.3 实现带记忆的具体 Skill自动存档笔记只讲接口不动手是纸上谈兵。下面我来写一个最实用的示例 Skill它负责自动记录闲聊笔记同时记住用户多次说过喜欢什么、讨厌什么。这个 Skill 在演示里效果非常直观比给人“算数学题”更能体现记忆的价值。# skills/notes.py from skill_base import Skill import json import os class NotesSkill(Skill): name notes_keeper description 把用户对话中的重要信息记录到长期笔记中能记住用户的偏好和关键事实 parameters { type: object, properties: { content: {type: string, description: 需要记录的具体信息}, tags: {type: array, items: {type: string}, description: 信息对应的标签比如偏好或关键事实} }, required: [content, tags] } def __init__(self, memory_storeNone): super().__init__(memory_store) self.notes_file memory/notes_store.json self._load() def _load(self): if os.path.exists(self.notes_file): with open(self.notes_file, r, encodingutf-8) as f: self.memory json.load(f) def _save(self): os.makedirs(memory, exist_okTrue) with open(self.notes_file, w, encodingutf-8) as f: json.dump(self.memory, f, ensure_asciiFalse, indent2) def execute(self, **kwargs): content kwargs.get(content, ) tags kwargs.get(tags, []) if not content: return {status: empty} for tag in tags: self.memory.setdefault(tag, []).append(content) self._save() return {status: ok, stored: content, tags: tags} def recall(self, key: str, defaultNone): return super().recall(key, default)实际运行效果是这样的当 Agent 收到一句“我最近在学吉他提醒我每天练琴和做笔记备注”LLM 判断这需要执行 notes_keeper解析出的 content 是“在学吉他每天练琴加笔记”tags 是[偏好, 日程]然后 Skill 把这个片段写进 JSON。第二次对话时Agent 能主动从记忆中抽取相关内容再向用户确认是否更新。这个技能的成功关键在于描述文件里写清楚“该技能擅长处理哪类用户信息”描述写得好模型才肯主动用它。2.4 实现一个有主动性的 Skill主动询问上一次偏好一个真正有魅力的 Skill不应该只等着被调用还应该在调用时机不合适时“拒绝执行”或“主动反向澄清”。为了实现这点我需要给 Skill 增加一个前置判断方法 precheck它会在 execute 之前被调用。扩展基类加一行逻辑def precheck(self, **kwargs): return True, 然后在技能覆盖这个 precheck 方法。我对 NotesSkill 做改造如果用户要记录的内容和已有记忆发生冲突比如之前用户明明不喜欢喝咖啡现在又让 Agent 记录“我超爱咖啡”Skill 会主动返回“conflict”状态把冲突点带出来让 Agent 有权限追问用户“我记得你说过你不喜欢现在要覆盖吗”。你可以直接复制一个小片段测试def precheck(self, **kwargs): content kwargs.get(content, ) for vals in self.memory.values(): for v in vals: if 不喜欢 in v and 喜欢 in content: return False, f冲突之前记录过这句话——{v}当前内容可能和旧偏好矛盾 return True, 这样 Skill 就不仅仅是被动接受参数它拥有了自己的工作流判断能力这就是“主动性”落到代码层面的一个非常具体又容易上手表达的方式。3. 角色注入与人设管理让 Agent 有个性但也有边界3.1 为什么人设信息不能硬编码在对话历史里我见过好多同学的第一个 Demo 是把人设直接写在 user 消息前缀里比如在每轮用户问题前拼上一句“你是 AI 助手你有以下特点喜欢简洁回答、喜欢自称‘本喵’”。这种写法在 Demo 阶段能跑通但后续有三个问题第一每轮都把同样的人设塞进上下文白白浪费成百上千的 token还可能把用户最新消息挤出去第二一旦某轮对话历史很长人设会被大量聊天内容淹没模型很容易出现人设叛逆第三人设与业务状态杂糅不利于调优。正确做法是把角色定义放到 System Prompt 区域。System Prompt 在模型推理时的权重区别于普通用户消息而且能稳定地约束模型风格、行为准则和边界设定。我的设计方案是做一个 UserProfile 配置类把角色信息集中管理并且允许 Skill 动态往这个配置里追加临时行为约束。3.2 角色配置的核心结构与边界逻辑下面是一个可以落地的最小人设配置结构# profile.py class UserProfile: def __init__(self): self._roles [] self._constraints [] def add_role(self, role_text): self._roles.append(role_text) def add_constraint(self, constraint): self._constraints.append(constraint) def build_system_prompt(self): lines [] lines.append(你是用户的个人 AI 助理名字叫小记。) if self._roles: lines.append(角色特质 .join(self._roles)) if self._constraints: lines.append(边界约束 .join(self._constraints)) return \n.join(lines)我建议在 System Prompt 里写清楚三层内容第一层是身份定义用一句话说清楚“你是谁”第二层是能力清单列出这个 Agent 手上有哪些 Skill第三层是行为边界告诉模型哪些事可以做、哪些情况不能做、哪些场景必须停下来向用户二次确认。不要让人设里堆积大量“你很聪明”“你很贴心”这种空话模型不需要这种自我催眠式夸奖。真正带来行为差异的是约束性描述比如“当用户提到想删除某个记忆时你必须先复述一遍要删除的内容让用户确认才可以执行”这种可执行规范比形容词重要得多。3.3 让 Skill 动态增强角色感知能力实现 Skill 与角色配合的机制其实有两条路径可以走。路径一是在 execute 方法内部主动向 Agent 反向返回新的人设建议由 Agent 决定是否采纳路径二更简洁让 Skill 在返回执行结果时携带一个 profile_append 字段这个字段会被 Agent 主循环捕获临时追加到本轮的 System Prompt 后面。我自己的经验是优先做第二条因为它的侵入性最小逻辑也清晰。写一个“当用户表达情绪低落时Skill 返回结果里附带‘这轮回答请保持温和鼓励’”的场景效果显著。这个机制让 Skill 像是一个人身上的神经系统末梢它感知到特定场景后能对整个人的表达风格做动态微调。3.4 角色切换一次对话里遇见多个“子人格”也不崩当你的 Agent 要同时服务“学习助手、健康教练、工作日程管理”三种不同职能时单一角色很容易互相干扰。解决方案是采用角色栈把每个任务域拆成独立的子配置由一个 Router 模块每次判断当前用户请求应该启用哪个角色栈。拿工作中的例子说。某人让 Agent“帮我安排今天下午 3 点的健身训练”时从姓名和意图来看这明显更贴合“健康教练”的角色Agent 自动加载健康教练的角色约束比如要求回答简洁、要记录训练建议、要提醒不超过 60 分钟但在问候语层面又保留记忆里的个人称呼习惯。切换的过程中全局记忆保持共享角色局部记忆按角色分开存储这样做的好处是每一个子人格都能在任务域内积累更专业的用户记录。4. Agent 主动性闭环实现从“问一句答一句”到“自己规划下一步”4.1 主动性的基础工具Agent 主循环逻辑现在到了这次动手最核心的部分Agent 主循环。我们先把之前定义的 Skill 注册到 Agent 里然后通过一个 while 循环让 Agent 能在“思考—行动—观察结果—再思考”的节奏里推进任务。核心逻辑用伪代码描述是这样的1. 把 SystemPrompt 和当前对话历史发送给 LLM 2. 解析 LLM 返回内容判断是否存在要调用的 Skill 3. 若没有确定任务完成输出最终回复退出循环 4. 若有解析出 Skill 名、参数和相关上下文 5. 调用 SkillRegistry 执行技能拿到结果 6. 把结果以 tool 消息格式追加到历史记录 7. 回到 1 步继续循环这个循环结构看起来简单但它是 LLM 从“对话模型”进化到“Agent 模型”的分水岭。传统对话模型永远只根据用户输入生成一段文字而循环模型可以自我观察、执行动作、根据动作结果调整策略。第一次自己动手写这个循环时你会清楚地感受到“程序真正干事情了”的快乐。4.2 注册表实现让 Agent 知道自己在哪些方面有本事注册表的实现可以是轻量级的类我忍不住要提醒的是这个注册表的可用性决定了 Agent 后面的聪明程度。# skill_registry.py from typing import Dict, Type from skill_base import Skill class SkillRegistry: def __init__(self): self._skills: Dict[str, Type[Skill]] {} def register(self, skill_class: Type[Skill]): instance skill_class() self._skills[skill_class.name] instance def list_skills(self): return [ {name: s.name, description: s.description, parameters: s.parameters} for s in self._skills.values() ] def execute(self, name: str, **kwargs): skill self._skills.get(name) if not skill: return {error: fskill {name} not found} ok, msg skill.precheck(**kwargs) if not ok: return {status: precheck_failed, reason: msg} return skill.execute(**kwargs) def get_skill(self, name: str) - Skill: return self._skills.get(name)注册一个技能只需一行代码registry SkillRegistry() registry.register(NotesSkill)每次 Agent 发起请求前我让 registry.list_skills() 的结果直接拼接到 SystemPrompt 的工具说明区域这样模型能读取到当前有哪些技能可用。为了让技能调用更稳定我会把描述做成一个极简的“技能名——一句话说明——参数示例”然后传送给模型。4.3 如何用输出格式让 Agent 返回“可执行动作”让 LLM 主动决定调用哪个 Skill有两条技术路线可走。第一是使用服务厂商提供的 tool calling 函数即原生 function calling 机制第二是通过约定输出格式让模型按 JSON 格式返回动作。第一版最简单、也能避开各家 SDK 对 function calling 实现不一的坑可以选择第二种方式。我在 SystemPrompt 中安排这样的指令当用户请求需要执行具体能力时请输出如下 JSON{action: notes_keeper, args: {content: 用户说的话, tags: [偏好]}}如果不需要调用技能就只输出正常文本回复。解析函数只需简单地用 json.loads 尝试解析正常文本会出现 JSON decode error此时直接返回文本给用户这就自然地实现了分支判断。注意这里非常容易踩坑模型可能输出 JSON 时附带解释性文字比如会先输出“好的我来帮你记录”然后再输出 JSON。所以解析函数需要设计成先提取文本里第一个json...代码块再尝试 json.loads解析失败也不应该直接抛出异常而是把原始文本视作普通回复。4.4 完整主循环的 Python 代码演练我这里写一个可以直接跑最简实验的 agent_core.py其中只保留标准 OpenAI 兼容接口的调用逻辑# agent_core.py import json import re from openai import OpenAI from skill_registry import SkillRegistry from profile import UserProfile class DemoAgent: def __init__(self, base_url, api_key, model_name, profile: UserProfile, registry: SkillRegistry): self.client OpenAI(base_urlbase_url, api_keyapi_key) self.model_name model_name self.profile profile self.registry registry self.history [] def _extract_json(self, text): match re.search(rjson(.*?), text, re.S) if match: return json.loads(match.group(1).strip()) try: return json.loads(text.strip()) except: return None def run(self, user_input: str, max_steps: int 5): self.history.append({role: user, content: user_input}) step 0 while step max_steps: system_parts [self.profile.build_system_prompt()] skills_desc json.dumps(self.registry.list_skills(), ensure_asciiFalse) system_parts.append(f你当前可以使用的技能清单{skills_desc}) system_parts.append(如果用户请求需要执行某种技能返回json包裹的动作。) messages [{role: system, content: \n.join(system_parts)}] self.history response self.client.chat.completions.create( modelself.model_name, messagesmessages, temperature0.7 ) content response.choices[0].message.content action self._extract_json(content) if action and action.get(action): result self.registry.execute(action[action], **action.get(args, {})) self.history.append({role: assistant, content: content}) self.history.append({role: tool, content: json.dumps(result, ensure_asciiFalse)}) step 1 continue self.history.append({role: assistant, content: content}) return content return 步骤超限未能完成目标。加上这段循环以后Agent 的行为就已经和聊天机器人有了质的不同。你和一个没有主循环的接口说“帮我记一下我最近在学吉他”它可能只会回你“好的已记住”但不会真正写盘。而这里的结果是模型输出一个 action 后代码会真实地调用 NotesSkill.execute 把笔记写进文件并把写入成功的结果重新喂回模型模型收到结果后再说出“我已经帮你保存在长期笔记里了”这句话。整个过程里Agent 自己充当了决策者和验证者不只是输出一段友好回复而已。4.5 限制循环层数与防止 Agent 陷入死循环主循环虽然强大但不加约束也会变成脱缰野马。我在别处见过一个很蹩脚的 Agent 无限循环调用同一个查询技能而不自知最后把上下文撑爆还闹了接口账单超支的笑话。所以在设计循环时必须加三个保险最大轮数上限、重复动作检测、总上下文长度限制。重复动作检测的思路是维护一个 action 的记录列表如果连续三次出现同样的技能名和几乎一样的参数就强制退出本轮循环并向用户说明“这个操作可能无法通过当前手段完成”。上下文长度限制则在构建 messages 前统计历史的总字符数超出阈值就把早期消息做一个摘要替换。保底手段非常朴素但能让我在调试阶段省下大量翻车时间。5. 长期记忆的工程化从内存字典升级到文件型记忆库5.1 什么样的信息值得写进长期记忆在没做信息筛选的 Agent 里长期记忆就是一个垃圾场什么都往里扔调用的时候又什么都翻不出来。我先分享一个筛选准则“能被复用的行为偏好与客观事实优先存储针对于单次任务状态的临时数据不要干扰长期记忆。”用户随口说的“今天天气不错”不值得存“我希望以后回复我时不要堆满表情”值得存“我在 2026 年 3 月 10 日下午 3 点要开周会”可以存成近期日程事项“周会结束了”就应该从日程里移除而用户“喜欢用列表而不是长段落”这个偏好是长期稳定信息必须一直保留。我建议定义两种记忆类型并分别处理Episodic Memory事件记忆记录任务进行中的临时对话要点Daily Cleanup 或 Expire Policy 定期清理Semantic Memory语义记忆抽取用户稳定偏好尽量写得规范化。上面的 NotesSkill 示例把一切写进同一个 JSON虽简明但还不够工程严谨。要是你想把它做成产品级还需要在数据结构里加 createdAt、sourceRole 和 expireAt 三个字段。5.2 如何把多轮对话关键内容归结成一条条可检索的记忆摘录信息抽取是不能靠简单正则完成的正确手段是让大模型自己承担“记忆提炼师”的角色。给模型一个小型提炼 Prompt让它在每轮对话结束后从最近的消息里抽取值得长期保留的事实与偏好然后传输给 NotesSkill 执行入库。提炼 Prompt 可以这样用你是记忆助理。请从对话中提炼需要长期记住的信息。 要求 - 只提取用户明确表达的偏好、身份信息、长期目标、重要约定 - 忽略一次性寒暄、语气词和临时状态 - 每条信息控制在 40 字以内 - 返回 JSON 数组用这个提炼 Prompt 得到的输出往往质量高很多。我在设计 Agent 主循环时会在用户明确表达了“记住”类关键词时触发一次记忆提炼平时则选择会话结束时统一批量提炼。这样既省 token又不会每轮都打扰主任务执行。5.3 记忆冲突处理与用户修改记忆的方式长期记忆运行一段时间以后大概率会出现内容前后矛盾。最经典的场景是用户先告诉你“我一般 11 点前入睡”隔两周又说“帮我定个凌晨 1 点提醒我要赶稿”。这时你的 Agent 必须能够识别冲突并向用户求证。可以给 Agent 增加一个查询类 Skill专门负责读取记忆内容再把已有记录与新信息丢回给 LLM 判断是否冲突。判断逻辑用简单规则硬编码也可以过一段时间再升级成向量相似度对比不过在 demo 阶段规则关键词匹配足够解决问题。Skill 返回冲突状态后Agent 如果选择“确认覆盖”NotesSkill 就需要有幂等合并的能力按同一个标签覆盖旧值而不是简单追加。记忆修改权限也需要控制好。原则上读取记忆时所有技能都需要预授权写记忆时必须通过通用记忆管理 Skill不允许其他技能绕过记忆校验直接写库。如果你不加这个管理口很容易出现多个技能各自在自己的文件里存出一份不一致的用户画像。6. 让 Agent 学会使用工具为 Skill 增加主动联网查询与数据检索能力6.1 最小工具扩展一个可以接收搜索词的 SkillAgent 的主动性在架构上与工具调用天然是一对所以很有必要把上面写的 NotesSkill 和下面要实现的网络查询技能做一个串联。我这里实现的联网查询技能本质是一个可替换请求函数它会接收 search_query 参数通过请求一个公网搜索 API 返回若干条搜索结果摘要。# skills/web_looker.py import requests from skill_base import Skill class WebLookerSkill(Skill): name web_search description 用给定的查询词访问搜索服务返回网页搜索结果摘要。 parameters { type: object, properties: { search_query: {type: string, description: 搜索关键词}, max_results: {type: integer, description: 返回结果数量} }, required: [search_query] } def execute(self, **kwargs): query kwargs.get(search_query, ) limit kwargs.get(max_results, 5) if not query: return {error: empty search query} # 假设通过一个公开接口这里演示只打印实际请求前会访问的地址。 response requests.get( https://example-search.local/search, params{q: query, n: limit}, timeout10, ) response.raise_for_status() items response.json().get(items, []) return {query: query, items: items[:limit]}你实际上会用一个可用的搜索服务替换上面的函数重点是要理解对 Agent 而言“联网搜索”并不神秘它就是向外界发一个 HTTP 请求拿到新内容后再喂回模型。整个链条中搜索工具是 Skill 的一种而 Skill 又是 Agent 的一种能力。6.2 工具返回结果的清洗与压缩工具调用完之后给到模型的结果不能是原始 HTML 或超大 JSON模型虽然上下文窗口在变大但把无关噪声塞进去既浪费 token 又拉低注意力。我习惯在返回前做三层清洗去 HTML 标签、截断每段摘要到 120 字内、只保留和原始查询最相关的标题与摘要。当然也可以用更聪明的方案先让一个小模型对返回内容做提取再传给主 Agent但对多数场景基础清洗已经够用。压缩尤其重要因为搜索结果 10 条可能就上千字而真正影响 Answer 质量的往往是前三条。所以我在 execute 的最后再加一个 sort_by_relevance 参数如果为真就用模型对内容做提取排序。多步执行的 Agent 在每一步都应该让上下文的增量保持精炼这样未来叠加更多技能时不会窘态百出。6.3 示例场景演示让 Agent 自己判断是否要查资料一个用户提问“帮我看看 2026 年 AI Agent 方向发展有什么新趋势再基于这个总结三条可以推荐给团队的实践”。Agent 主循环收到这个问题后应该会自己判断需要联网查询于是返回一个 actionweb_search 的 JSON。执行完查询后Agent 拿到若干条结果摘要再结合用户的历史偏好比如用户以前提过“希望输出结构化要点”生成一个既有外部资料支持又有用户个性化风格的回复。整个操作闭环的体验和大家在各类 AI 套壳应用里的“联网搜索”明显不一样因为这里搜索是由 Agent 根据当前目标和已有记忆主动决策触发的而不是用户每次手动打开联网开关。触发时机的判断靠的就是 register 里那句描述信息足够清晰这个描述是影响后续行为最敏感的小点。7. 让 Agent 更贴近真实使用测试、调优与一次完整实战7.1 怎么避免 Agent 在调用 Skill 时胡猜参数让模型从自然语言里提取结构化参数总会出问题尤其在用户没按最佳格式说话的时候。比如我给 NotesSkill 定义的是 content/tags 两个参数用户在对话里直接说“记一下 7 月 20 日和周杰伦打羽毛球”模型很有可能把整个自然句塞进 contenttags 为空数组。解决思路是让每个技能都在参数描述里更清晰说明要如何切割自然语言。我还是用 JSON Schema 的 description 字段描述。content: { type: string, description: 将自然语言整理成简明的存储语句时间与人物要保留去掉无关语气词 }, tags: { type: array, items: {type: string}, description: 从内容中提取的标签包含对象、时间类型、任务类型三类最多3个 }还有一个实用技巧给每个 Skill 配一段“典型调用示例”在 SystemPrompt 里附上“如果用户说『提醒我 15 号交房租』你应该输出如下 JSON”这比纯讲参数 Schema 更高效因为模型对少量示例的模仿准确率往往远高于对新格式的推理能力。我用了几轮调试后发现 2-3 个典型示例能够大幅减少解析失败的情况。7.2 建立你个人的 Agent 行为评测集写 Agent 很容易陷入“昨天调通今天改坏”的循环所以要趁早建一个非常小的评测集。我自己的做法是准备一个 pytest 文件里面放 8 到 10 条固定的用户输入然后为每条输入定义三个断言目标是否成功触发了正确的 Skill、Skill 执行结果是否符合预期、最终回复中是否包含关键回复成分比如在笔记库里能查到新记录。可以把这些评测输入看成 Agent 领域的单元测试def evaluate(agent, test_cases): score 0 for case in test_cases: agent.run(case[input]) expected_skill case[expected_skill] actual agent.registry.get_skill(expected_skill) if actual.recall(last_status) ok: score 1 print(f测试通过率 {score}/{len(test_cases)})我刚起步时会用 LLM 当评委给回复质量打分不过后来发现最可靠的方式还是验证 Side Effect也就是检查技能执行之后系统的外部状态是否有变化。你是否真的写入了一条笔记、是否真的查询并返回了数据这些比主观回复质量更容易断言。如果追求快速迭代这个“结果验证”的检查应当优先于“文本流畅度”的评审。7.3 一个完整实战案例从入门介绍到最终带记忆的回复为了确认你有直观的理解我给出一次完整的跑通对话示例。这个用户新到项目开场问“你叫什么你能做什么” Agent 加载人设后回复“我是小记可以帮你记录待办事项偏好信息也能联网查资料”。同时主循环发现这只是一个询问并没有需要调用的技能。接着用户说“我记得我之前说过我反感把回复搞得很长。这次的总结要精简用三条帮我总结一下 AI Agent 的现状吧。”这轮主循环的处理流程是这样的Agent 先从长期记忆里查到了“反感过长回复”的记录然后判断“总结 AI Agent 现状”需要联网查询于是先调用 NotesSkill 查询旧的偏好再调用 web_search 启动搜索。搜索拿到几条外部资料后Agent 结合“倾向简洁”的约束生成回复最终输出三条简明要点并在结尾附加一句“对了你之前提到过反感长总结所以这次我就压缩成这样了”。用户看到这句的时候能被产品体验打动因为这里的主动性和记忆让 AI 从工具变成了一个有服务意识的协作者。上面这段完整流程我建议你按前面代码跑通以后再去逐条观察每一条日志才能真正明白 Agent 每一步交互的作用。7.4 常见问题与排查技巧实录结合自己的经验和社区反馈我把最常见的故障和解决方法整理成下面查速表对你跑代码非常有参考价值。现象常见原因排查与修复模型从不调用 Skill只输出普通文本技能描述不清晰、注册表没拼进 Prompt打开注册日志检查 SystemPrompt 里是否真的携带技能列表把描述改短并补充触发场景技能返回 JSON 解析失败模型输出的 JSON 被自然语言前缀污染用正则提取 json 代码块解析失败时不要抛错走普通文本分支反复循环同一个动作Agent 缺少失败终止逻辑检查动作记录若连续三次相同参数就中断长期记忆没生效引用的 memory_store 不是同一个对象保证 SkillRegistry 初始化时注入公共记忆字典不同模块不要各自 new 一个新 store上下文越来越长导致成本暴涨主循环把所有 tool 执行结果原样追加进历史对结果做清洗压缩超过一定长度只保留摘要角色偶尔跑偏SystemPrompt 与其他历史消息互相干扰用角色栈每次根据请求路由切换在边界规则里强调“不执行……”比强调“要执行……”更有效技能 check 发现冲突但用户没感知precheck 返回信息只存在于代码内部返回信息需要让 Agent 以追问形式反馈给用户实现对人机共识的确认7.5 Skill 开发中的安全意识清理技能权限与危险操作一个拥有工具调用能力的 Agent 一定要添加权限边界尤其当 Skill 会发起 HTTP 请求、读取本机文件或执行系统命令时。我的底线要求是“最小权限”每个 Skill 只能在自己声明的文件路径或接口域名下工作任何跨域操作必须先由白名单检查并且在执行敏感操作前强制用户二次确认。再提醒一句如果你让 Agent 具备调用任意 Python 代码或任意 shell 命令的能力就等同于给别人开放了一个可任意执行代码的接口。如果你把 Agent 做成了 API 服务更需要立刻引入审计日志把每一次动作、参数、触发用户都记录下来。Agent 对行业效率的提升毋庸置疑但它带来的“主动行为”风险不能只靠侥幸来兜底。8. 下一步进化如何把所有知识滚动成更大的 Agent 能力圈8.1 Skill 和角色的边界机制让单个 Agent 可以持续扩展把这篇的内容做完后你手里应该已经有一个能跑会记、能搜索、有角色意识的 Agent 底座。接下去要扩展能力的路径变得很直白想让它会查数据库就写一个 query_db 的 Skill想让它会发邮件就写一个 send_email 的 Skill想让它会写代码并执行就给这个 Skill 配上沙箱执行环境并加白名单。每个新技能只需要遵循标准接口注册到注册表里然后改一下 SystemPrompt 的描述即可。单个 Agent 能拥有的 Skills 数量理论上不受硬限制但是如果你注册了 20 个 Skills把 20 个技能描述全部塞进 Prompt 会影响模型决策准确率。解决方案是再加一层“技能推荐器”由分类标签对用户请求做初筛只把最可能的 3-4 个技能说明发给 LLM。这就像在搜索场景里先召回再排序。到这一步你的 Agent 已经是一个具备多能力选择和路由的家庭版调度器了。8.2 向团队复用经验Skill 也可以被导出与分享2026 年这个时点社区里已经出现“分享 Skill”的习惯人们把自己花了很多轮调出来的高价值技能模板化、参数化卸载给别人的 Agent 使用。好的 Skill 可以被粗略分成“技能描述元文件 执行脚本 测试集”三个文件放到一个目录以后下载方只要配一次环境就能完整复用。我自己的经验是给 Skill 写“设计说明”比写“使用说明”更重要越离谱的触发场景和越明确的边界案例能帮助后来者理解这个技能存在的目的。例如“在用户表达低落情绪时不建议调用功能型技能优先调用安慰型话术模板”这种经验说明一旦记录下来就变成团队内部可积累的隐性知识资产。8.3 小记从个人玩具到工程化应用之间隔的几次自我反思Agent 的开发过程很适合用一句大实话来总结不要把 Agent 想得太玄它无非是一个能在循环里调用外部能力的对话程序也不要把 Agent 想得太浅要让它长期不出错你要维护的反而是一套记忆质量体系、角色边界规则与技能测试集。我个人最受用的一条经验是每隔一段时间回看自己的 Agent 运行日志总结哪些技能实际被高频调用、哪些技能描述词总是导致模型误触发、哪些角色约束发生了预期之外的冲突。Bug 不在代码里而是在 Agent 对世界的模型构建偏差里这一条适用于任何技能开发。如果你正打算从零开始写自己的第一个 AI Agent我建议从今天这篇文章里直接复制最小骨架先跑通一个带记忆的 notes_keeper 技能再做联网查询体验闭环以后再去学各种繁复框架。把一个能自己主动记忆并关联上下文的小 Agent 真正跑在本地你会突然明白 Agent 开发里所谓“智能感”并不神秘它可以被拆成每一天可迭代的工程细节。
返回列表