如果你刷技术圈比较勤,应该已经注意到AI Agent这个词快被聊烂了。但说实话,大部分谈 Agent 的文章都在讲概念、讲架构图,真正能跟着一步步落地的实操分享反而少。我自己从最早写死 Prompt 玩大模型,到做成有状态、有技能包、能自己决定下一步动作的东西,中间踩的坑比想象中多得多。这篇文章我就用完全手把手的方式来拆一遍,标题是“手把手书写你的第一个 AI Agent:当 Skill 有了记忆、角色和主动性”,重点就在记忆、角色、主动性这三个词上。这三个词恰好是普通 Prompt 封装和真正的 Agent 之间最本质的分界线。
我不会只贴一段代码给你看,而是把每一步背后的设计依据、取舍逻辑、容易忽略的细节全部铺开讲。看完之后,你应该能在自己的项目里复现一个带 Skill 机制、有短期长期记忆、有人设约束、具备基本主动调度能力的 Agent 雏形。这玩意不是玩具,很多商业化产品的底座也就是这么长的。
1. 先把核心问题聊透:没有 Skill 的 Agent,和大模型套壳有什么区别
1.1 痛点来自 Prompt 工程的三个天花板
我先说一个场景,你自己体会一下是不是遇到类似情况。假设你想让模型扮演一个“嵌入式开发助手”,帮用户分析日志、生成寄存器配置、检查 GPIO 初始化代码。如果只是用 Prompt 把角色设定写死,再扔给模型,你会遇到三个类问题。
第一,模型调用的“技能”全藏在提示词里。不管你是做日志分析、DTS 修改还是 build 错误排查,本质都是上下文模板拼接。日志格式一变,Prompt 就得改。更难受的是,当任务链路长起来,比如“分析日志 -> 定位外设异常 -> 生成补丁 -> 跑静态检查”,这个流程逻辑是代码里写死的,模型根本没有自主规划的空间。
第二,每次都像失忆一样。上一步刚确认完芯片型号是ESP32-S3,下一步让它生成初始化代码时它又开始问你“请提供您的芯片型号”。对话历史不是没传,而是传了也不一定能稳定约束后续决策,尤其当上下文超过一定长度,早期关键信息被稀释得很厉害。
第三,模型没有主动性。它以“提问-回答”的单轮范式活着,你问一句它回一句。真正干活的时候,你希望它能自己决定“我需要先看一下寄存器手册里的说明,再决定怎么改配置”,然后真的去调工具、读文档、尝试、校验、出结果。这已经不是提示词能解决的范围了。
这三个痛点是环环相扣的。技能碎片化导致能力边界模糊,记忆缺失导致决策质量低,主动性缺失导致整个系统只能停在“高级聊天机器人”层面。而围绕Skill构建的自定义 Agent 架构,恰好就是冲着这三点去的。
1.2 Skill 和 Agent 的关系:不是包含,而是“能力的具象化”
这里要先厘清一个概念。很多人把Skill理解成“给 Agent 准备的一套提示词模板”,这太窄了。还有一种说法是 Skill 就是外部工具函数,这也不够。我倾向把 Skill 看作是“Agent 可以调用的一组带描述、带输入输出契约、带执行逻辑、带状态读写权限的能力单元”。
举个好消化的类比。把 Agent 想象成一个真人新员工。这个人聪明、理解力强,但他刚入职,什么业务都不会。他需要几样东西才能干活:岗位说明书(角色定位)、工作手册(Skill 定义)、记事本(记忆系统)、以及主管授权他主动推进任务(主动性)。所以 Skill 不只是“知识”,它还包含行为逻辑。
在这个框架下,Agent 是执行者,Skill 是执行者可选择的行为包。Agent 为什么要“有” Skill?因为 Skill 把模型不可控的自然语言能力,转化为可控的、可复用的、带约束的模块。模型负责判断“我现在该调用哪个 Skill、如何组合”,Skill 负责把模型意图翻译成确定性逻辑。这个分工是 Agent 架构里最核心的一层。
2. 设计 Agent 的四个要素:记忆、角色、主动性、工具调用
2.1 记忆机制:不只是塞上下文,而是做状态分层
标题里第一个关键词是“记忆”。我给你一个简化的记忆分层模型。
- 短期记忆:当前任务上下文里产生的关键信息,比如用户刚刚指定了项目目录、选了某种通信协议。放内存或会话变量里即可。
- 长期记忆:跨会话保留的偏好、历史结论、常用配置。需要持久化,轻量方案是 SQLite 或本地 JSON 文件,重型方案可以上向量数据库。
- 工作记忆:Agent 当前的思考草稿、中间结果、计划清单。这一层容易被忽略,但它决定了 Agent 能不能“边干边记”。
实现上有一个非常实用的设计模式:把记忆封装成 Agent 可读写的工具,而不是直接改系统 Prompt。也就是说,模型通过调用“写入记忆”这个动作来留存信息,通过“检索记忆”这个动作来获取历史相关内容。这样做最大的好处是,记忆的存取时机由模型根据任务自主决定,而不是每一轮都把所有历史无脑拼进 Prompt。
这个设计有直接好处:上下文长度可管理,关键信息不会被冲散,跨会话体验能真正形成。我在自己项目里用 SQLite 做了个极简版,字段就四个:记忆编码、记忆类型、记忆内容、创建时间。Agent 检索时按关键词匹配和创建时间排序取前几条,效果已经比硬塞上下文好非常多。
2.2 角色设定:不是“你是一个助手”这么简单
角色这个词很多人做得太浅。早期我写 Agent,角色设定就是一句“你是一位资深嵌入式工程师”,完事。后来发现,这个“角色”对模型行为的约束非常弱,模型照样会一本正经胡说八道,照样会在不确定的时候乱猜。
真正有效的角色设定,应该包含三层信息。
第一层是身份边界:你能做什么,不能做什么。比如“你是代码评审助手,只负责库函数层面的静态分析,不做板级硬件调试”。第二层是输出风格约束:汇报的表达习惯、是否给出代码示例、遇到不确定时是明说还是猜测。第三层是价值观与安全边界:涉及到可能破坏系统的命令,必须二次询问。
这三层里,身份边界和风格约束很多人会写,但安全边界很少被重视。实操中,安全边界最好的实现方式不是写死在角色 Prompt 里,而是配备“拦截型 Skill”。比如你定义了一个执行Shell命令的 Skill,那这个 Skill 内部必须带危险命令校验逻辑,而不是靠模型自觉。角色负责“不主动越界”,Skill 负责“越界也执行不了”。两道闸门都装上,系统才可靠。
2.3 主动性:从响应式到任务自治的机制跃迁
主动性是这三个词里最容易被误解的。有人觉得主动 = Agent 不等人问,自己开口说“我来帮您……”,这其实只是表面的主动。真正的主动性是任务调度上的自治能力:当用户给定一个模糊目标,Agent 能自行拆解子任务、决定子任务的执行顺序、处理执行中出现的异常、甚至发现前置信息不足时主动查资料或提问澄清。
这套机制在工程上是靠“循环”实现的。经典的结构是Agent Loop,大致流程我整理成下面这个逻辑:
循环开始 1. 从任务队列中取出当前最优先的子任务 2. 模型根据「当前状态 + 可用 Skill 列表 + 记忆摘要」决定下一步动作 3. 如果动作是调用 Skill -> 执行 -> 把结果写回状态 -> 更新记忆 -> 回到第 1 步 4. 如果动作是回复用户 -> 输出最终结果 -> 结束循环 5. 如果动作是请求更多信息 -> 抛出澄清问题 -> 暂停循环等待用户输入这里最关键的技术细节是第 2 步。模型如何知道有哪些 Skill 可用?靠的不是它在训练数据里见过,而是你把 Skill 的名字、用途、参数说明喂给了它。每轮迭代前,Agent 都会拿着“当前任务 + Skill 清单”去决策,相当于每走一步都重新规划一次。这样的设计远比“预设好整个任务链路然后顺序执行”更鲁棒。
具体到实现,有两条路线。路线 A 是图遍历型:你预先画好状态机,Agent 在状态节点之间跳转。适合流程非常固定的场景。路线 B 是自由规划型:Agent 每次循环都自主选下一个动作,不受预设流程约束。适合探索性任务,也是现在公认更接近通用 Agent 的路线。新手建议从路线 B 入手,代码量不大,而且能直观感受模型“自己动脑子”的效果。
2.4 Skill 和 Function Calling 到底怎么分工
读到这里你应该有个感觉,Agent 手里的武器就是 Skill。那有的人会问,大模型本身支持 Function Calling / Tool Use,为什么还要自造一个 Skill 概念?这两者是什么关系?
我用大白话解释一下。Function Calling 是模型输出层面的一种结构化调用协议,它让模型学会输出“我想调用某个函数、传这些参数”。它解决的是“模型怎么把意图变成可执行指令”这个问题。但 Skill 是更高一层的抽象,它解决的问题是“这个能力单元内部怎么实现、怎么和 Agent 的上下文与记忆协作”。
也就是说,一个 Skill 内部完全可以调用多个 Function,也可以不发函数调用,只做文本处理再写回记忆。Skill 是对 Function 的编排和封装,同时它自身也可以有状态和记忆读写能力。理解了这层,你就不会被“接入大模型就算 Agent”这种说法带偏了。
3. 动手前必须选型的三个关键点
3.1 底层模型怎么选:不是越贵越好,要看你需要的“推理密度”
想做 Agent,第一件事是选模型。我的建议是别盲目上最大参数的商业模型,先用一个推理密度适中的模型把链路跑通。注意我这里说的“推理密度”,是单位任务里模型需要做多少次决策。
如果你的 Agent 是简单问答型,大部分模型都够用。但如果你的 Agent 需要自主编排工具、处理多层状态,那这个模型必须足够“听话”,不然你会看到它频繁发明不存在的函数名、漏传参数、修改记忆时格式错乱。就我个人经验,支持结构化输出、且对工具调用格式训练比较充分的模型,在 Agent 场景下体验会好不少。
还有一个务实的选择:把“主规划模型”和“子任务执行模型”分开。规划模型负责循环决策,可以用能力高一档的模型;一些机械执行型 Skill 内部直接调便宜模型甚至规则逻辑,没必要全走贵模型。开销能差出好几倍,这是个非常实用的小技巧。
3.2 Agent 框架和纯手写之间怎么选
市面上以 Agent 为卖点的框架不少,动不动就是编排运行时、插件市场,看起来很吓人。我个人的看法是:第一次做 Agent,请你先“手写骨架,再考虑框架”。
原因有两个。第一,框架封装度太高,你会迷失在抽象概念里,很难理解记忆、角色、主动性究竟是怎么流转的。第二,很多框架的核心玩法就是帮你拼接各类工具,但一旦你遇到不满足的边界场景,改框架内部的调度逻辑往往比重新写一个还麻烦。
反过来我也不劝你一根筋全手写。大模型 SDK、向量存储、数据库这些基础设施该用就用,但 Agent 的核心循环、Skill 注册表、记忆操作接口这些“灵魂组件”,建议第一版自己写一遍。这个“手写”的过程,才是真正建立技术直觉的地方,等你踩过一轮以后再去用框架,你会非常清楚框架的每个 API 解决的是什么问题。
3.3 任务场景要收敛:第一个 Agent 别“既要又要”
选场景这个事我要多说两句。很多初学者上来就想做一个“全能生活助理”,让它管日程、查天气、订外卖、写周报,结果一个都做不稳。这里有一个设计原则,叫能力半径受控。第一个 Agent 最好只做一类任务,但把这类任务做到闭环。
举例来说,“日志异常分析 Agent”就是个特别合适的起步场景。它只有三个 Skill:读日志文件、查历史故障知识库、生成分析报告。任务半径小,状态有限,记忆的作用明显,很容易验证效果。等这个跑顺了,再往上加 Skill、增加任务类型,就不会到处着火。我相当推荐这种从窄到宽的演进路径。
4. 从零搭建 Skill 运行时:目录、配置、加载机制
4.1 Skill 的目录组织与配置格式
我习惯将每个 Skill 拆成独立目录,放它的描述配置和实现逻辑。一个标准目录长这样:
skills/ ├── read_log_file/ │ ├── SKILL.md │ └── main.py ├── query_issue_kb/ │ ├── SKILL.md │ ├── main.py │ └── data_base.json └── generate_report/ ├── SKILL.md └── main.pySKILL.md是我给每个 Skill 配的“身份证”,里面写好 Agent 决策时需要用到的元信息。格式样例:
--- name: read_log_file description: 读取日志文件内容,支持按时间范围和关键词过滤。 when_to_use: 当用户要求分析日志、定位异常、筛选特定事件时使用。 parameters: - name: file_path type: string required: true description: 日志文件的绝对路径或相对路径。 - name: keyword type: string required: false description: 仅返回包含该关键词的行。 - name: since type: string required: false description: ISO 格式的开始时间,如 2026-08-01T00:00:00。 --- # read_log_file 该 Skill 负责读取本地日志文件,提取关键信息并做初步清洗。 实现中要注意大文件不可以一次读入内存,必须按行流式处理。这套配置看起来没什么玄机,但真正起作用的是when_to_use字段。模型在每个循环节点拿到全部 Skill 清单后,会先靠name和description做粗筛,再把候选 Skill 的完整配置拿出来细读。如果description写得太空泛,模型要么不选它,要么在无关场景误选它。新手最容易忽略的就是这个字段的调优,它非常影响 Agent 的工具选择准确率。
4.2 Skill 实现为什么要统一接口协议
每个 Skill 的main.py基本逻辑可以不同,但对外暴露的接口协议必须统一。我设计的最小协议大概这样:
class BaseSkill: skill_name: str def __init__(self, context: AgentContext): # context 里装有记忆句柄、配置、会话历史等 pass def execute(self, **kwargs) -> SkillResult: # 返回值统一为 SkillResult,内含状态四要素 passSkillResult我一般会带上四个字段:
output: 该 Skill 想传递给模型的核心信息,比如报告文本、查询结果。new_memories: 需要写入记忆存储的内容列表,由 Skill 主动产生。status:success、failure、require_more三选一。metadata: 额外追踪用的元信息,如耗时、写入条数。
为什么非要带new_memories字段?因为 Skill 在执行过程中很可能会发现有用的信息,比如读日志时发现某条 DMA 异常频繁出现,这个信息值得留存。如果 Skill 不能主动写记忆,那每个 Skill 的发现就只能通过大模型下一次读取输出文本来间接沉淀,既不准确又费 token。把记忆写入动作放进接口协议里,本质上是把“记忆沉淀”变成一种可编程的硬行为,而不是可选的软行为。
4.3 注册表与加载器:让 Agent 知道自己有什么能力
Skill 目录有了、协议的骨架有了,还需要一个加载器把它们都注册到 Agent 模型中。这步的核心是“构造 Skill 清单文本”,喂给模型做动作选择。写一个最简单的加载器:
from pathlib import Path import importlib.util def load_all_skills(skill_root: str, ctx): skills = {} for skill_dir in Path(skill_root).iterdir(): if not skill_dir.is_dir(): continue config_path = skill_dir / "SKILL.md" main_path = skill_dir / "main.py" if not config_path.exists() or not main_path.exists(): continue # 把 SKILL.md 的 front-matter 解析出来 meta = parse_front_matter(config_path.read_text(encoding="utf-8")) # 动态加载 main.py 模块 spec = importlib.util.spec_from_file_location( skill_dir.name, main_path ) module = importlib.util.module_from_spec(spec) spec.loader.exec_module(module) skill_instance = module.Skill(ctx) skills[meta["name"]] = { "meta": meta, "instance": skill_instance } return skills别小看这个加载器,它决定了你后续加新 Skill 是不是丝滑。当你新增能力时,只需要往skills目录丢一个带描述和实现的新文件夹,重启后 Agent 就能自动感知,主体代码一行都不用改。这套插件化思想是 Agent 能力能够持续生长的基础。另外,解析 front-matter 时注意容错,别让一个 Skill 的配置语法错误导致整个 Agent 启动崩溃。
5. 实现一个最小可复用的 Agent 核心循环
5.1 状态机的结构设计
前面概念讲了不少,现在进入到写代码环节。我会做一个面向“日志异常分析”的最小 Agent,它包含三个 Skill:读日志文件、查询历史故障知识库、生成分析报告。这个任务足够收敛,又能把记忆、角色、主动性全部串起来。
Agent 主循环的状态管理我直接用字典保存,完整代码大概是:
import json import uuid class Agent: def __init__(self, llm, skill_loader, memory_store): self.llm = llm self.skills = skill_loader self.memory = memory_store self.session_id = str(uuid.uuid4()) def state_initial(self, user_request): # 初始化状态:目标、任务描述、当前进度、决策历史 return { "session_id": self.session_id, "goal": user_request, "subtask_queue": [], "plan": [], "current_action_idx": 0, "done": False } def decide_next_action(self, state): # 构建 prompt:包含角色设定 + 当前状态摘要 + Skill 列表 + 可用记忆 messages = self.compose_messages(state) # 模型决定下一步动作,这里要求模型输出 JSON response = self.llm.chat(messages, response_format="json") return self.parse_action(response)这里最需要精心设计的是compose_messages这个函数,本质上是把状态、记忆、Skill 描述变成决策上下文喂给模型。我把它拆成三块来看。
角色设定块。每次循环都注入,但并不是一大段人设模板无脑堆叠,而是把当前任务目标、角色边界、行业背景写进去。例如:“你是嵌入式方向资深代码评审与日志分析助手,只基于给定日志和知识库信息下结论。禁止臆测寄存器位含义。若信息不足以得出结论,必须明确说明缺乏依据。”这里边就有安全边界和不确定处理策略。
Skill 选择清单块。把所有可用 Skill 的name、description、parameters都列出来,让模型在每轮决策时都能看到自己能调用的“兵器库”。这个动作倒不用每次贴全量。如果 Skill 数量大于十来个,优先让模型做一次粗选,再给粗选结果中的 Skill 暴露详细参数。
记忆摘要块。从长期记忆里检索与当前任务相关的记录,比如历史异常案例、用户偏好、上次处理结论。记忆摘要是缩短“重新理解成本”的核心,如果该块缺失,Agent 的“记忆”就名存实亡。
实际执行决策时,我要求模型返回 JSON,结构统一为:
{ "reasoning": "简要说明为什么选这个动作", "next_action": "call_skill|reply|require_more", "skill_name": "read_log_file", "parameters": { "file_path": "logs/uart.log", "since": "2026-08-01T00:00:00" } }模型输出了结构化指令,循环体就能像一个状态机一样往下走了。
5.2 让 Agent 真正跑起来的 Skill 执行器
写执行器时有一个细节非常关键:Skill的执行结果不一定是一次性成功的,它可能返回的是中间信息、也可能抛异常。因此执行器不能简单地result = skill.execute()就把结果塞给模型,必须做一个统一的包装和异常兜底。
def run_skill(self, state, skill_name, params): if skill_name not in self.skills: return { "status": "failure", "reason": f"unknown skill: {skill_name}" } skill_def = self.skills[skill_name] try: result = skill_def["instance"].execute(**params) except TypeError as e: # 参数不匹配兜底:把异常信息返回给模型自行调整 return { "status": "parameter_error", "message": f"参数校验失败: {e}。请检查参数说明后重试。" } except Exception as e: return { "status": "runtime_error", "message": f"Skill 执行异常: {e}" } return result针对参数不匹配的错误,我给模型返回了一个“请检查参数说明后重试”的信号,这里面有一个重要的工程经验:不要让循环因为参数错误直接崩溃,而要把错误交给模型去反思并修正。模型很多时候只是少传了一个可选参数,收到异常后它会自觉补调用参数。这种“模型自己修正自己”的机制,能显著提高多步骤任务的成功率。
循环体在执行完 Skill 后必须做一次“记忆吸收”,把new_memories和中间观测写入记忆存储。同时还需要更新状态字典里的决策历史,格式简单,追加一条{action, skill_name, result_summary, time}即可。这些决策历史将在下一轮决策中跟记忆摘要合并,作为模型的输入,形成对任务全局的掌控感。
5.3 写一个能“记忆前文”的 Skill:内部状态如何串起来
为了展示“记忆”并非只能靠数据库,我特别演示一个带内部状态翻转的 Skill。这个 Skill 叫write_analysis_snapshot,它负责把每一步的中间分析结果累积到一个会话级快照文件里。
# skills/write_analysis_snapshot/main.py import json from pathlib import Path class Skill: name = "write_analysis_snapshot" def __init__(self, context: AgentContext): self.context = context self.snapshot_path = context.data_dir / f"snapshot_{context.session_id}.json" def execute(self, content: str, step: int): # 读取既有快照,不存在则初始化 snap = {} if self.snapshot_path.exists(): snap = json.loads(self.snapshot_path.read_text(encoding="utf-8")) snap[f"step_{step}"] = content self.snapshot_path.write_text( json.dumps(snap, ensure_ascii=False, indent=2), encoding="utf-8" ) return { "output": f"已保存第 {step} 步分析快照。", "new_memories": [{ "type": "snapshot_update", "content": f"写入快照 step_{step}", "session_id": self.context.session_id }], "status": "success", "metadata": {"step": step, "path": str(self.snapshot_path)} }这个例子的启发性在于,Skill不再只是无状态函数,它可以持有context,操作会话级文件,把一次任务过程中产生的中间状态记录下来。这对应到现实场景就是:Agent 分析了二十个日志文件后,突然想回看第三步猜的寄存器配置,不再需要从头推演,因为快照已经在那里了。这比“让模型自己记住”可靠得多。
5.4 加一点“主动性”的味道:Agent 自检与继续任务循环
如何体现主动性?我加一个叫self_check的调度能力。它的触发条件不是用户指令,而是 Agent 在主循环中发现“子任务尚未完成且状态不充分”的时候。比如日志分析 Agent 读到一个中断风暴关键词,但当前日志时间范围只覆盖了后半段,它应当自己决定“我需要扩大时间范围重新读一次日志”,而不是停下来问用户“您看接下来怎么办”。
这在代码里表现为一个分支判断:当模型决策返回的next_action是call_skill,且目标 Skill 是read_log_file时,循环体在更新完状态后不结束,继续进入下一轮决策。真正让“主动”落在工程上的,是给模型一个开放的动作空间,并且明确告诉他“如果发现结果不完整,你有权调用request_range_expansion这类 Skill 来自行补充任务”。
我在状态机里额外维护了一个attempt_count,每执行一轮循环就加一。当它大于阈值(比如 8),循环强制退出,把当前摘要和已确认结论交给用户,并对未决问题做透明度说明。设置这个上限是防止模型在一个错误的方向上反复自我修正,永远跑不完。这里同样体现了一个 Agent 设计原则:既要给自由,也要给边界。
6. 落地一份可查的记忆:长期记忆与短时记忆的实现策略
6.1 SQLite 方案与 JSON 方案对比
关于记忆存储,很多教程会把向量数据库说成默认选项,但我不建议你一上来就上重型组件。以最小闭环为目标,你只需要回答三个问题:记忆存哪?按什么查?如何避免膨胀?
如果你只有几十条记忆,放在 JSON 文件里最方便。每次循环读取整个文件、过滤出相关记录丢给模型就行。缺点是数据量上来后,全量读取浪费 token,也拖慢响应。另一个方案是 SQLite,轻量、无需独立服务、支持条件查询,适合存几千条以下的记忆记录。这两种方案前期的代码复杂度差别不大,SQLite 加一个标准库即可搞定,我推荐直接上 SQLite,免得后期迁移。
表结构我就三张表,极其简单。
-- 会话表 CREATE TABLE sessions ( session_id TEXT PRIMARY KEY, user_id TEXT, created_at TEXT ); -- 事件事实表 CREATE TABLE memories ( memory_id INTEGER PRIMARY KEY AUTOINCREMENT, session_id TEXT, memory_type TEXT, -- preference / fact / task_result / snapshot content TEXT, created_at TEXT ); -- 记忆来源索引表 CREATE TABLE memory_links ( link_id INTEGER PRIMARY KEY AUTOINCREMENT, memory_id INTEGER, source_skill TEXT, source_step INTEGER );这种设计刻意不去碰向量索引,因为初期记忆规模很小,SQLite 的LIKE查询配合时间倒序已经足够。只有当记忆条数奔着十万条去、且你真的需要“语义相似召回”时,才引入向量检索,这条路后头好接。
6.2 记忆写入时的“归纳”习惯
这条是我实际用的经验,印象很深。刚开始我的 Agent 把日志原文大段写入记忆,没过多久记忆库就膨胀得没法看了,每次检索都捞出一堆垃圾。后来我加了一条约束:Skill 写记忆前必须先做归纳,而不是搬运全文。
我在read_log_file的逻辑里加了剪裁函数,超过一定长度的内容不直接入记忆,而是摘出关键行、数量统计、级别分布,再让大模型用一句话总结发生了什么。这个抽象的过程其实给 Agent 记忆质量带来了质的提升。记住,记忆的目标不是“存档”,而是“让未来的自己快速理解以前发生了什么”。降低记忆写入的冗长度,是长期维护 Agent 最重要的一步。
6.3 记忆的过期与优先级:让 Agent 知道什么该忘
这个点很多文章讲不到,但我认为非常关键。记忆如果永远只增不减,总有一天会把有效信息淹没。我建议每个记忆带一个priority字段,从 0 到 5。偏好类记忆设置为 5 分,例如“用户偏好 C 语言实现方案”;临时过程信息设置为 1 分,例如“某次运行输出的第一行是 SPDIF data”。检索时按(priority DESC, created_at DESC)拉取,低于阈值的记录不进入模型上下文。
同时,增加一个简单的每日清理动作:删除priority <= 1且创建时间超过 24 小时的记忆。这让 Agent 的长期记忆更有弹性,等任务跨周以后你回头看,这个设计能让你少掉很多头发。
7. 给 Agent 注入角色感:约束 Prompt、上下文模板与风格护栏
7.1 角色模板的工程化写法
前面说了角色要分三层,我这里给一个可复用的模板结构,放在项目里叫agent_brain.md:
# Agent 角色说明书 你是「日志侦探」,一名资深的嵌入式系统日志分析助理。 你擅长解析 UART/SPI/I2C 日志,定位异常驱动、中断风暴、时序紊乱等问题。 ## 身份边界 - 只依据用户提供的日志与知识库文件进行分析。 - 禁止在没有寄存器手册的情况下猜测具体寄存器位含义。 - 不提供任何可导致设备物理损坏的操作建议。 ## 工作流程建议 1. 先读取日志,了解事件全貌。 2. 发现可疑模式时,查询历史故障知识库。 3. 结合记忆中的历史结论,形成推理链条。 4. 最终输出结论时列出完整证据链。 ## 输出风格 - 先给结论,后给证据。 - 每条结论需要有对应日志行作为依据,并标注行号。 - 如果信息不足,直接说“当前信息不足以支撑该结论”。你可能会说,这和普通 Prompt 有什么区别?有,但要配合代码才会生效。我不会每轮循环无脑丢全部角色说明书,而是把它切块按需注入。只有在出现“是否需要怀疑方向”的决策点时,才注入“工作流程建议”块;只有在要生成最终结论时,才注入“输出风格”块。这既控制了 token 开销,也让模型在不同阶段注意力更集中。
7.2 输出风格护栏:让模型不“跑偏”的方法
角色设定里有一部分不是给模型看的,而是给后处理逻辑看的。我管它叫输出风格护栏。它可以实现为在模型生成完结果后跑一个轻量后处理函数,检查结果中是否包含“证据引用缺失”等违规情形。
举个例子,日志分析 Agent 的最终报告要求每条结论必须有对应日志行号。后处理函数逐个检查结论文本,如果某条结论后没有形如L123的标注,就自动插入一句“该结论缺少行号参考,已跳过”。别高估模型每次都会自觉遵守格式要求。端到端任务里,越往后模型越容易松懈,没有后处理护栏,报告格式最终会跑偏得很随性。
7.3 人设的“动态切换”机制
一个 Agent 可以同时服务多种角色吗?可以,但建议你给每个角色做一个独立的角色上下文包,而不是把多角色塞进一段 Prompt 里来回切换。我用一个叫“角色上下文分流器”的方法:在请求入口处先让模型做一次轻量的意图分类,把“让我看看日志”分到日志分析 Agent 通道,把“帮我写段代码”分到代码生成 Agent 通道。每个通道内部都用完全独立的角色设定、Skill 清单、记忆库。
这有个直接优势:不同域的记忆天然隔离,日志领域的观察不会污染代码生成领域的判断。同时每个 Agent 能力范围变小,错误率也会降下来。很多商用 Agent 产品本质上就是多个“单角色 Agent”的外壳路由,这套架构不花哨,但非常稳。
8. 完整示例:一个可复现的“带记忆与主动性的日志分析 Agent”
8.1 项目目录与代码骨架
我直接给你一份可以直接跑的最小工程结构,方便你照着抄。
log_agent/ ├── agent_brain.md ├── agent_core.py ├── llm.py ├── memory_store.py ├── skills/ │ ├── __init__.py │ ├── base.py │ ├── read_log_file/ │ │ ├── SKILL.md │ │ └── main.py │ ├── query_issue_kb/ │ │ ├── SKILL.md │ │ └── main.py │ ├── write_analysis_snapshot/ │ │ ├── SKILL.md │ │ └── main.py │ └── generate_report/ │ ├── SKILL.md │ └── main.py └── data/ ├── sqlite.db └── logs/ └── uart.logmemory_store.py里封装 SQLite 的读写函数,核心接口四个:init_db()、add_memory()、search_memory()、cleanup_expired()。llm.py则做一层适配,把通义、OpenAI 风格 SDK 统一封装成chat(messages, response_format)和embed(text)两个方法。
agent_core.py就是前面展示的主循环加执行器实现,组装起来大概是这个流程:启动时加载 Skill 和记忆,把用户请求放入初始状态,循环调用decide_next_action直到done=True或到达轮次上限。
8.2 跑一个完整任务演示
假设用户在交互框输入:“帮我分析data/logs/uart.log里为什么 UART 偶发乱码,并给出建议。”
第一轮循环,模型读到角色说明书 + Skill 列表 + 当前状态。它选择调用read_log_file,传参file_path=data/logs/uart.log和keyword=ERROR。执行器把日志中所有包含ERROR的行读出来,返回给模型并记录一条摘要记忆。
第二轮循环,模型在决策历史里发现日志中存在多个RX overrun事件。它知道项目学过的历史故障知识库里有overrun的相关案例,于是调用query_issue_kb,参数是keyword=RX overrun。知识库返回了历史处理中的一条经验:先查 FIFO 中断阈值配置,再查 DMA 描述符是否耗尽。
第三轮循环,模型发现这两条信息已经可以支撑初步结论,但还缺一次完整印象。它决定调用write_analysis_snapshot把前两轮的发现落一份到快照文件,留给后续分析沉淀。
第四轮循环,模型调用generate_report。它综合日志证据、知识库经验和快照记录,生成一份包含三个证据点、两个建议项的最终报告。输出风格按角色说明书要求,每条证据都带日志行号。到这里工作流结束,Agent 返回报告给用户。
这就是一个完整的“有记忆、有角色、有主动性”的 Agent 工作过程。你看着很自然,但如果没有 Skill 的封装、没有记忆的沉淀、没有每一轮“自己决定下一步”的循环机制,单靠一次大模型调用是不可能走完这个链路的。
8.3 效果评价与调优方向
跑完一轮后,怎么确认这个 Agent 是合格的?我用的评价项很简单:任务完成率、工具调用准确率、记忆检索准确率、最终报告的事实一致性。前三个指标可以靠人工抽样统计,最后一个我会专门让另一个 LLM 实例做裁判,检查报告结论是否能从执行日志中找到证据支撑。
如果任务完成率不高,先别着急换模型。你按这个顺序排查:Skill 描述是不是准确;角色说明书是否约束了不当输出;记忆检索有没有带回正确信息;执行器异常兜底有没有把修正机会还给模型。这个排查顺序几乎覆盖了我的所有实战调优场景。
9. 常见问题与排查技巧实录
9.1 为什么 Agent 总是选错 Skill 或重复执行同一步
这个问题我遇到最多,也最容易被归咎于“模型笨”。实际上多数原因是 Skill 清单里的描述太相似了,尤其是name、description和when_to_use区分度不够,模型犯迷糊。
排查建议是,把全部 Skill 名称和描述打成一个大表,站在模型视角看能不能一眼分清“什么时候选谁”。如果自己也分不清,说明描述写得过于抽象。重写时要尽量写清适用场景和反面场景,比如read_log_file的描述可以写成“当你需要读取/查看一个日志文件的原始内容或筛选后的内容行时使用。如果用户希望直接得到分析结论而不是原始日志内容,推荐使用analyze_log技能”。
重复执行同一步的问题通常由状态更新不及时引起。要检查模型决策完有没有把动作追加到decision_history,并让新状态真正参与下一轮compose_messages。很多时候历史没喂回去,模型以为自己还没看过某个文件,于是反复读同一份日志。
9.2 模型输出乱 JSON 或虚构参数怎么处理
现在的大模型虽然支持结构化输出,但偶尔仍然会出现格式不合法、参数名臆造问题。建议从两个层面预防。
第一个层面是模型层约束:要求模型按 JSON Schema 输出,并设定temperature较低。第二个层面是代码层约束:增加一个repair_json函数,用容错解析逻辑过滤掉散落文本,直接把最外层{...}提取出来再解析。如果解析还是失败,将原始输出返回给模型,并在提示里明确说明“你的输出 JSON 解析失败:{错误信息},请只输出合法 JSON”,给它一个“补考”的机会。
参数名虚构通常是 Skill 配置里没有对参数做严格枚举。最简单有效的办法是在 Skill 描述文本里把参数说明再次写清楚,甚至给示例值。模型看到示例以后往往能瞬间学会正确传参,这比在代码里做一堆参数校验更省事。
9.3 记忆库膨胀、上下文超限怎么处理
记忆库膨胀最典型的征兆是:每次决策的输入里带上了一大堆过期的旧信息,模型决策速度变慢且容易被噪声误导。我之前靠 SQLite 的priority和created_at字段做了衰减处理,已经能缓解一大半问题。如果仍然上下文超限,可以在compose_messages阶段按三类做截断:角色说明书只保留关键片段;记忆摘要取最近 5 条加最高优先级 5 条;决策历史只保留最近 3 轮摘要。
还有一个技巧,每次模型执行完一轮 Skill 后,不要把它返回的所有文本原封不动存进记忆库,而是先用一个压缩摘要函数把关键结论提取出来。这个压缩可以由 LLM 完成,也可以直接采样关键句。压缩后入库的记忆大大降低后续上下文的压力,效果立竿见影。
9.4 主动性的边界:Agent 自己越想越偏怎么办
自由规划型循环最怕的一件事情是:模型在某个错误的中间结论上,反复调用 Skill 自我验证,形成死循环。比如它怀疑某个中断标志被错误置位,于是反复读寄存器配置文档,每次都得不到明确答案,然后它继续换着关键词查,任务迟迟不结束。
我的实操解法是双重限制:第一,限制每个任务总循环次数,比如 8 轮;第二,在decide_next_action的提示中显式加入一条规则——如果最近两次动作里已经包含三次同 Skill 调用且没有新事实产生,允许你在不调用 Skill 的情况下直接整理已知信息并给用户一个阶段性的结论,不要强行追求完美答案。这种“允许在不确定中收尾”的指令,反而是让 Agent 更可靠的关键,它减少了无意义的空转。
9.5 Skill 数量多了之后,Agent 决策反而变差怎么办
这是个非常现实的问题。Skill 清单一两页时还好,数量涨到十个以上,模型每次都要在长上下文里挑工具,偶尔就会漏掉更优的工具。解决路径有两条:一条是分层路由,首页只展示几个粗颗粒度的入口 Skill,比如“日志分析”“代码生成”“知识检索”,具体执行子 Skill 由入口注册时、选择后的局部清单来暴露。另一条是对 Skill 做向量检索匹配,先按文本相似度召回 Top 5 个候选,再把这些候选的描述交给模型精确决策。第二种方案在规模大的场景更有优势,适合后边需要扩展的读者尝试。
10. 我个人踩过的一些坑
最后分享几个非常具体、但教科书里不会写的经验。
第一,Skill目录的命名尽量用项目主题相关的短横线风格,不要用中文名。我发现有些框架解析时能处理中文文件名,但日志追踪和异常报错信息里一旦混入中文路径字符,排查起来会痛苦得多。
第二,Agent Loop里的每轮请求最好在消息结构里带上当前的动作序号和总规划,哪怕是两句话的摘要,也能显著增强模型对任务阶段的感知力。比如状态里写一条progress: "第2步/共5步,已完成日志读取,正在查询知识库",模型下一轮决策时会更像在“顺着进度往下走”而不是重新开始找切入点。
第三,不要省掉“记忆写入成功”的反馈信号。每次 Skill 返回new_memories并且写入成功后,要把“已成功记忆以下内容:xxx”反馈到模型上下文。模型如果知道自己的动作产生了持久化影响,后续决策会明显更笃定。这个小改动听上去很玄学,但我实测下来确实能让 Agent 的行为显得更“有意识”。
第四,如果你做的是面向记录、知识积累类的 Agent,强烈建议每一轮循环都把Skill调用参数和返回结果的摘要保存到本地日志,方便之后复现问题和复盘。Agent 是一个动态决策系统,不像普通程序那样“输入相同输出相同”。没有决策留痕,你根本无从判断某次劣质输出是模型抽风、记忆误导还是 Skill 本身报错。这个审计日志是你调试 Agent 时最重要的抓手。
这套东西看起来有不少概念,其实核心就一句话:Skill是 Agent 的行动抓手,记忆是它的长期底盘,角色是它的行为边界,主动循环是它的引擎。四者拼到一起,它才不再是一个“你问一句、它答一句”的聊天机器人,而是像一个真正有经验的新人——知轻重、记得事、会自己往后走。
我第一次把这套循环跑通的时候,看着模型自己在日志里查出隐患、查过历史案例、做出带证据链的报告,还是挺震撼的。希望这篇一步步拆解的实操内容,也能帮你体会到这种“把模型变成能干活的人”的感觉。写完第一版之后,不要停在这里,你可以再给它加新的 Skill、换更大的记忆方案、调整角色模板,让它越来越接近你想要的那个形态。