我最早被自己写的 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, default=None): 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_store=None): 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", encoding="utf-8") as f: self.memory = json.load(f) def _save(self): os.makedirs("memory", exist_ok=True) with open(self.notes_file, "w", encoding="utf-8") as f: json.dump(self.memory, f, ensure_ascii=False, indent=2) 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, default=None): 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": f"skill {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_url=base_url, api_key=api_key) self.model_name = model_name self.profile = profile self.registry = registry self.history = [] def _extract_json(self, text): match = re.search(r"```json(.*?)```", 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_ascii=False) 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( model=self.model_name, messages=messages, temperature=0.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_ascii=False)}) 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 最小工具扩展:一个可以接收搜索词的 Skill
Agent 的主动性在架构上与工具调用天然是一对,所以很有必要把上面写的 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}, timeout=10, ) 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 主循环收到这个问题后,应该会自己判断需要联网查询,于是返回一个 action=web_search 的 JSON。执行完查询后,Agent 拿到若干条结果摘要,再结合用户的历史偏好(比如用户以前提过“希望输出结构化要点”),生成一个既有外部资料支持又有用户个性化风格的回复。
整个操作闭环的体验和大家在各类 AI 套壳应用里的“联网搜索”明显不一样,因为这里搜索是由 Agent 根据当前目标和已有记忆主动决策触发的,而不是用户每次手动打开联网开关。触发时机的判断靠的就是 register 里那句描述信息足够清晰,这个描述是影响后续行为最敏感的小点。
7. 让 Agent 更贴近真实使用:测试、调优与一次完整实战
7.1 怎么避免 Agent 在调用 Skill 时胡猜参数
让模型从自然语言里提取结构化参数总会出问题,尤其在用户没按最佳格式说话的时候。比如我给 NotesSkill 定义的是 content/tags 两个参数,用户在对话里直接说“记一下 7 月 20 日和周杰伦打羽毛球”,模型很有可能把整个自然句塞进 content,tags 为空数组。
解决思路是让每个技能都在参数描述里更清晰说明要如何切割自然语言。我还是用 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 开发里所谓“智能感”并不神秘,它可以被拆成每一天可迭代的工程细节。