ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

从LLM到Agent:Skill设计与架构实践

从LLM到Agent:Skill设计与架构实践

1. 内容整体设计与思路拆解

1.1 为什么需要从LLM走向Agent

我在过去一年里的工作重心,几乎经历了从"怎么调好一个LLM"到"怎么让LLM真正干活"的转变。最早接触大模型的时候,主要做的是提示词工程、微调这几件事,本质上是让模型"回答得更准确"。但用着用着就会发现,单纯调用LLM存在一个非常明显的天花板:它输出的是文本,不是行动。

比如说,我问模型"帮我查一下最近一周的天气",它背出一段"天气受高气压影响"之类的知识,却不知道怎么拿到我所在城市的真实天气数据。再比如让它处理好几个Excel文件,它连打开文件的动作都做不到。说白了,LLM是一个极其博学的"顾问",但它没有手、没有脚,没有能力去操作外部世界。而Agent的出现,就是为了解决"语言能力"和"行动能力"之间的落差。

Agent的本质,是把LLM放在一个循环里,让它担当"大脑"的角色,通过计划、推理、调用外部工具、观察结果,最终完成任务。这个过程中,LLM负责最核心的思考部分,而真正执行具体动作的,就是一层一层封装好的"技能"。

我在实际项目里体会到,如果没有Skill这一层的抽象,Agent开发会迅速失控。LLM每一步都输出自由文本,你很难约束它"下一步该调用什么工具",而Skill本质上是给LLM圈定了能力和边界,让它知道:遇到什么情况,可以用哪个工具,工具的输入输出长什么样。这个概念想通了之后,整个Agent体系的搭建就顺了。

1.2 Skill在Agent架构中的位置

初次接触Agent的开发者,往往会被各种概念搞晕。Planner、Executor、Memory、Tool、Skill、Action这几个词经常混着出现。我用一句话概括它们的关系:Agent是一个有大脑、有记忆、有工具的人,Skill就是这个人身上"已学会的专业技能"。

以我目前常用的架构设计为例,它大概是这样的结构:

  • 底层是LLM推理引擎,负责理解任务、拆解步骤、产出决策。
  • 中间层是Agent控制层,负责维护状态、调用Skill、解析结果。
  • 上层是Skill层,每个Skill封装一个特定的功能,比如数据库查询、文件读写、网页抓取。
  • 最外层是交互层,接收用户请求,展示Agent最终产出。

这里的Skill跟Tool(工具)不太一样。Tool往往是单个可调用的函数,比如"执行SQL"、"播放音频";而Skill更像一个完整的能力包,它可能包含多个Tool的组合,还自带一套提示词约束和调用逻辑。举个例子,"数据库分析Skill"可能包含了"探查表结构""执行查询""生成分析报告"这三个Tool级别的操作,以及一整套引导LLM如何做数据分析的指令。

用生活里的例子打比方:Tool就像工具箱里的一把扳手,Skill则是"会修水管的师傅"——光有一把扳手不够,师傅还得知道先拧哪边、用多大力气、怎么判断是否修好。这些"经验域"和"调用策略"都写在Skill里面,所以Skill比Tool高一个抽象层级,更适合管理。

1.3 整体设计思路:三层架构与模块解耦

我在做第一个真正落地的Agent时,踩过一个大坑:把所有逻辑都写在一个主循环里,融资料查询、文件生成、邮件发送于一体。刚开始跑通的时候还挺兴奋,但到后面需要新增能力时,整个文件改得面目全非。后来参考了不少开源项目的设计思路,才下定决心把架构拆开,做成"LLM核心 + Agent调度 + Skill扩展"的三层结构。

这三层各司其职:

  • LLM核心:不关心业务,只负责根据当前语境生成下一步动作决策;
  • Agent调度层:轮询LLM输出,管理任务状态,把不同的Skill串成工作流;
  • Skill层:自己管好自己的输入输出约束,不依赖其他Skill的细节。

这样拆解带来的好处很实际。第一,新增一个Skill不需要动Agent主循环的代码,按接口规范写好注册即可;第二,调试某个Skill时,可以脱离整个Agent系统单独测试,定位问题非常快;第三,多个Agent项目可以共用同一套Skill库,我手头的自动化框架已经从最早的互相纠缠,重构为十几个独立Skill的集合,复用率明显提升。

设计Skill层的时候,还需要考虑一个容易被忽视的维度:技能之间是否有依赖关系。比如"生成周报"Skill可能要依赖"获取数据"Skill。我习惯在Skill定义里显式声明依赖,并让Agent调度层维护一个简单的依赖解析器。思路并不复杂,但提前设计比运行期临场处理要省事得多。

2. 核心细节解析与实操要点

2.1 LLM能力边界与调用方式

做Agent开发,首先要对LLM本身有一个清醒的认知,不然容易被它流畅的输出带偏。我在项目里对LLM的定位是:一个概率化的推理引擎,不是一座准确的知识库。很多人觉得模型答错是"幻觉",我觉得更准确的理解是:它在做概率生成,碰到没把握的内容会倾向于填入合理的猜测。

所以当你把它接入Agent系统时,核心策略是"不让它直接凭记忆生成关键事实",而是让它通过Skill去获取数据。比如我们做一个政策问答Agent,从不让LLM直接回答政策条款,而是先触发"检索类Skill"去资料库里查原文,再让LLM基于查到的内容做总结。这个思路跟RAG(检索增强生成)是同一个道理,只是放在Agent框架下,检索这一步也被包装成了Skill。

调用方式上,现在的LLM一般都支持Function Calling机制,也就是说你可以给模型声明一些函数的名称、参数和描述,模型在需要时会输出一个结构化的函数调用请求,而不是纯自然语言。这是Agent得以落地的关键技术,也是Skill注册表与LLM之间沟通的"协议层"。

我用过几个主流模型的Function Calling接口,整体格式大同小异,核心都是:系统侧提供一个工具描述列表,模型的回复里带上一个tool_calls字段,里面包含了模型决定调用的函数名和参数JSON。开发者只需要解析这个JSON,去执行对应的Skill,再把执行结果返回给模型,让模型继续推理。这套循环正是Agent最核心的"感知-思考-行动"模式。

2.2 Agent核心循环:ReAct模式与状态维护

在Agent框架里,最经典的执行模式是ReAct——Reasoning(推理)+ Acting(行动)。它的流程是:拿到目标任务后,LLM先在"思考"阶段分析现在的情况、打算采取什么行动,然后输出一个行动命令;系统执行命令、拿到结果;结果回传给LLM,让它进入下一轮思考。如此循环,直到LLM认为任务已经完成,输出最终答复。

我自己在实际复现ReAct模式时,关注两个容易被忽视的点。第一个是"状态维护"。Agent循环运行几轮之后,上下文里的历史输出越来越多,如果全塞给LLM,一方面费用会上升,另一方面模型容易被淹没在冗长的日志里,抓不住主线。我的做法是设置一个"工作记忆"缓冲区,只保留最近几轮的思考痕迹、行动结果和关键中间变量,超过轮次上限就做摘要压缩,效果比无脑拼接全文好很多。

第二个是"终止条件"设计。循环没有明确的结束标志,模型可能一直输出行动请求,形成死循环。我在循环外设置了两层保护:第一层是轮次上限,一般情况下任务在5次行动内完成,超过12次就强制中断,要求模型总结目前的进展并给出部分结论;第二层是"完成标志"识别,让LLM在认为任务已完成的回复里带上特定的结束标记,程序识别到标记后就不再让模型继续行动。这两层保护看起来简单,但实实在在地解决了Agent失控风险中很大的一部分。

2.3 Skill的粒度与设计原则

Skill设计得合理不合理,直接决定一个Agent好不好用。我在反复迭代中总结出了几条原则,接下来逐一细说。

第一条是"单一职责"。一个Skill只做一件具体的事。我最早犯过的错是把"查询数据库"和"生成报表"放在同一个Skill里,结果写的时候一时爽,改的时候满盘都是牵扯。后来强制规定:每个Skill的职责说明不超过一句话,如果描述超过一句话,说明这个Skill应该拆开。

第二条是"输入输出显式化"。LLM调用Skill时,输入参数需要从自由文本里提取出来映射到字段上,输出则要固定成结构化格式。如果Skill的输出格式不稳定,后续的Agent推理环节就很容易被带偏。我习惯给每个Skill定义清晰的输入输出Schema,类似调用一个API函数的规格说明。

第三条是"错误返回也要结构化"。Skill执行失败是常态,比如网络不通、数据为空、权限不足。关键在于,失败信息要能被LLM理解并指导下一步决策。我见过不少项目把Skill异常简单地打印成一行错误堆栈,LLM根本看不懂。更好的做法是返回一个包含错误类型、原因描述、建议应对方式的JSON结构,这样Agent就懂了怎么调整策略。

下面我用一个小表格整理Skill设计时的关键考量点,方便对照检查:

设计维度推荐做法常见错误
职责范围单一职责,一个Skill只做一件事把多个功能塞进一个Skill
输入定义明确的参数名、类型、必填项依赖LLM猜测自由文本字段
输出格式固定JSON结构,包含数据与状态返回自由文本,格式不可控
错误处理结构化错误码 + 原因 + 建议抛异常或返回堆栈信息
依赖管理显式声明依赖其他Skill直接调用其他Skill的内部函数
描述文本用LLM看得懂的语言写明适用场景描述含糊,模型不知道该不该调用

2.4 提示词约束:让LLM"会开技能菜单"

Skill设计好之后,怎么让LLM知道什么时候该用哪个Skill,同样是一门学问。我的经验是:不要指望LLM自然就知道Skill的适用场景,得在系统提示词里给它一份"技能菜单说明书"。

这份说明书跟普通对话时的系统提示不一样,它需要更偏向"决策树"风格。每一个Skill的说明至少包含这几个要素:Skill名称、概述、触发条件(什么情况下使用它)、输入参数说明、输出示例、使用限制。LLM在决策时就像翻菜谱一样,先看需求匹配哪道菜,再按菜谱准备材料。

我还发现一个细节:触发条件写得越贴近具体场景,模型调用准确率越高。举例来说,如果一个Skill叫"计算器",触发条件写"当用户需要进行任何数学计算时使用",效果就比"计算相关功能"要好得多。因为LLM判断的是用户请求和技能描述之间的语义匹配度,描述越具体,匹配越准确。

另外,我在实测中总结出一个提效技巧:给Skill的启用场景配备"反例"。就是在描述里明确告诉模型,哪些情况不应该用这个Skill。比如"天气查询Skill"后面加一句"当用户询问的是历史气候数据而非实时天气时,不要调用本Skill"。有了反例约束,模型的误调用率肉眼可见地下降了。

3. 实操过程与核心环节实现

3.1 环境准备与基础框架搭建

说完了设计,下面进入实操环节。我用一个"数据查询与摘要生成"的Agent为例,完整演示从零搭建一个带Skill的Agent。这个案例虽然简单,但它涵盖了Agent开发的基本链路,后面做更复杂的项目都是在这个框架上做加法。

我用的技术栈是Python配上目前比较通用的LangChain生态。模型部分我建议用支持Function Calling的中等规模模型进行开发和测试,成本低且调试方便。基础环境安装就不详细说了,需要重点说明的是框架选择思路:LangChain这类框架最大的价值不是它自带的那些高级链路,而是它把"工具注册—模型调用—结果回传"这个循环做好了,省去你自己去写一遍底层的函数调用解析。

安装好环境后,我习惯先把最朴素的"裸循环"跑通,而不是一上来就套框架的高级API。裸循环的代码大致是这个样子:

from openai import OpenAI client = OpenAI() messages = [ {"role": "system", "content": "你是数据处理助手,负责按流程调用工具完成用户任务。"} ] tools = [ { "type": "function", "function": { "name": "query_database", "description": "在本地数据库执行SQL查询并返回结果", "parameters": { "type": "object", "properties": { "sql": {"type": "string", "description": "要执行的SQL语句"} }, "required": ["sql"] } } }, { "type": "function", "function": { "name": "generate_summary", "description": "对传入的数据生成一段简洁的中文摘要", "parameters": { "type": "object", "properties": { "data": {"type": "string", "description": "需要总结的原始数据"} }, "required": ["data"] } } } ] def call_llm(messages): resp = client.chat.completions.create( model="your-model-name", messages=messages, tools=tools, tool_choice="auto" ) return resp.choices[0].message

这一步的意义在于验证两件事:第一,模型能不能正确理解工具定义;第二,循环解析函数调用有没有bug。框架的封装再漂亮,底层原理也是这套。彻底搞懂这个循环之后,再用框架的高级API就顺手多了。

3.2 编写第一个Skill:从注册到执行

接下来是一个完整的Skill实现。我定义两个Skill:query_database和generate_summary。按照之前的设计原则,每个Skill对应一个解析函数、一个执行函数、一个结构化输出。

import json import sqlite3 def execute_query_database(sql: str) -> dict: """ Skill: query_database 职责: 在本地测试库中执行SQL查询 """ try: conn = sqlite3.connect("test.db") cursor = conn.cursor() cursor.execute(sql) rows = cursor.fetchall() columns = [desc[0] for desc in cursor.description] conn.close() return { "success": True, "columns": columns, "rows": rows, "row_count": len(rows) } except Exception as e: return { "success": False, "error_type": type(e).__name__, "error_message": str(e), "suggestion": "请检查SQL语法或表名是否存在" } def execute_generate_summary(data: str) -> dict: """ Skill: generate_summary 职责: 对输入数据生成摘要 """ summary_prompt = f"请用不超过三句话概括以下数据的关键信息:\n{data}" resp = call_llm([ {"role": "user", "content": summary_prompt} ]) return { "success": True, "summary": resp.content }

Skill执行函数和工具函数是两个概念,这个区别我刚开始没搞清。工具函数是纯函数,指做什么操作;Skill执行函数负责把LLM传过来的参数转换成工具函数能用的格式,再把结果翻译回LLM看得懂的结构化消息。

执行完Skill之后,最关键的步骤是把结果"回注"给LLM。这一步我见过很多人做得不够好,直接把原始结果塞回去就完事,导致LLM面对一堆冗长的数据库输出不知所措。正确的做法是,在回注结果时加上简短的"前情提要":

def send_tool_result_to_llm(tool_call_id, result_dict): summary = f"工具执行完成。状态:{'成功' if result_dict['success'] else '失败'}。" if result_dict["success"]: summary += f"共返回{result_dict['row_count']}行数据。" else: summary += f"错误原因为:{result_dict['error_message']}。" return { "role": "tool", "tool_call_id": tool_call_id, "content": json.dumps(result_dict, ensure_ascii=False, default=str) }

这里我额外加了一个"人类可读的前置摘要"字段,放在结构化JSON之前。实测下来,这能显著减少LLM读取长结果的出错概率,类似给一个纯技术报告贴上执行摘要,先让模型知道大概发生了什么事。

3.3 Agent主循环与完整运行示例

下面把上面的模块串起来,写成Agent主循环。我用一个简化版本,重点展示循环控制逻辑:

def run_agent(user_query: str, max_rounds: int = 5): messages = [ {"role": "system", "content": "你是数据查询助手。当用户需要查询数据时,先调用query_database,再使用generate_summary总结结果。"}, {"role": "user", "content": user_query} ] skill_executors = { "query_database": execute_query_database, "generate_summary": execute_generate_summary } for round_idx in range(max_rounds): assistant_msg = call_llm(messages) if not assistant_msg.tool_calls: return assistant_msg.content messages.append(assistant_msg) for tool_call in assistant_msg.tool_calls: func_name = tool_call.function.name args = json.loads(tool_call.function.arguments) if func_name not in skill_executors: error_result = { "success": False, "error_type": "UnknownSkillError", "error_message": f"未知技能:{func_name}", "suggestion": "请只使用提供的技能" } messages.append(send_tool_result_to_llm(tool_call.id, error_result)) continue result = skill_executors[func_name](**args) messages.append(send_tool_result_to_llm(tool_call.id, result)) return "已达到最大执行轮次,任务可能未完整完成。"

我用一个实际的执行场景来演示效果。假设用户问"帮我查一下销售表中各地区的订单总数,并简要总结"。执行过程大概是:

  • 第1轮:LLM输出tool_call,调用query_database,参数SQL是"SELECT region, COUNT(*) FROM orders GROUP BY region";
  • 系统解析参数、执行数据库查询,返回成功结果;
  • 第2轮:LLM结合查询结果,判断还需要调用generate_summary来总结数据;
  • 系统执行摘要生成,返回总结文本;
  • 第3轮:LLM判断任务完成,直接输出最终答复,不再产生tool_call。

这段日志清晰地展示了ReAct模式在三轮之内解决一个组合任务的完整链路。一个值得注意的点是,LLM在"要不要再调用一次工具"这件事上,判断通常比较保守——倾向于多调用而不是早结束。所以在设计任务时,尽量把目标拆到"刚好需要调用特定Skill"的颗粒度,不要让模型频繁尝试无谓操作。

3.4 技能注册表与管理

随着Skill数量增加,管理它们就成了一门新的功课。我搞了十几个Skill之后,开始用一个注册表来统一管理。

class SkillRegistry: def __init__(self): self._skills = {} self._tool_defs = [] def register(self, skill): self._skills[skill.name] = skill self._tool_defs.append(skill.to_tool_definition()) def get_tool_definitions(self): return self._tool_defs def execute(self, skill_name: str, **kwargs): skill = self._skills.get(skill_name) if not skill: raise UnknownSkillError(skill_name) return skill.execute(**kwargs)

注册表带来的好处不只是"集中管理",更重要的是它统一生成了LLM所需的tools列表。每次新增Skill,只要写一个类并register,前端不用做任何改动。当我需要临时禁用某个Skill时,只需从注册表移除它的tool定义,模型自然就"不知道"有这个技能存在了。

注册表还有一个进阶用法——给Skill分区。比如分成"数据类""文件类""网络类",每个区对应不同的安全权限。数据类Skill不允许操作非查询语句,文件类Skill不允许删除目录,这类约束用注册表做统一控制,比在各个Skill内部各自判断要可靠得多。

3.5 进阶:让Skill具备自我描述能力

做到这里,Agent已经能跑通"查询-分析-总结"流程了。但还有一个值得深挖的方向:让Skill具备"自我描述能力"。这个思路来自我对Agent自我认知的思考——如果Skill只有静态描述,模型对它的理解是有限的;反过来,如果Skill能动态回应模型的询问,Agent对任务的理解就会深得多。

实现方式也不复杂。在每个Skill模块中加入一个describe_capability方法,返回这个Skill在当前数据环境下能做什么、不能做什么。比如query_database这个Skill,可以在描述里动态带上"当前数据库中存在以下数据表:orders, customers, products",模型据此就能生成更贴合实际数据的SQL查询。

我实际测试过一个场景:Agent被问到"帮我看看客户购买力分布",如果没有动态描述,模型可能会尝试查询一个不存在的字段;而动态描述让模型先通过describe_capability获取到表结构,生成查询时准确率大幅提高。这个模式可以看作是RAG在Skill层的一个变体——不是检索文档片段,而是检索"技能能力清单"。

4. 常见问题与排查技巧实录

4.1 Function Calling解析与参数匹配问题

在所有Agent开发的问题里,Function Calling的解析出错是出现频率最高的。我在初期几乎每天都会遇到这样的情况:LLM输出的tool_call参数是合法JSON,但字段类型跟我的预期不一致。比如SQL参数本该是字符串,模型传了一个数组;数据类型本该是浮点数,模型做成了字符串。

排查这类问题,我总结了一套流程。第一步,打印完整的原始响应,不要自己解析后再打印——很多时候问题就出在解析过程中,比如用了不兼容的JSON解析库。第二步,对比模型响应的参数和tools定义里的参数,看是否因为描述不够清晰导致模型误判类型。

第三步最关键:检查是否把"推理"和"执行"混在了一起。模型有时会在工具参数里填入一个"打算如何做"而不是"要求程序做什么"。比如让它执行SQL查询,参数却传了"SELECT * FROM orders WHERE..."再加一段"命令解释"文本。这是模型对工具语义理解不足的表现,解决办法不是在代码里做各种容错,而是回到Skill的描述文本,把参数定义写得更精确。

我后来在参数schema里加了一个"example字段"——给每个参数附上一个明确的示例值。效果立竿见影,参数类型错误率下降了一截。虽然这个字段大部分API规范里不是必填的,但强烈建议加上。

4.2 模型"坚持不调用Skill"的问题

有时候问题反过来:明明Skill定义得很清楚,LLM却偏偏不调用它,直接基于自己的知识回答。这种情况最常见于"用户的提问方式和Skill描述之间的语义距离比较远"时。比如我给知识库检索Skill写了"当用户询问事实性问题时使用",但用户问的是"那个规定的有效期到什么时候",模型可能觉得这是在打听时间,而不是需要查文档。

针对这种情况,我从两个方向做了优化。第一个方向是补全Skill的别名,在描述里列出尽量多的近义表达。比如"查询"类Skill的描述里加上"查找、获取、搜索、定位、确认信息"。第二个方向则是调整系统提示词里的任务拆解指引,明确告诉模型:"对于可能涉及事实/数据的问题,请先调用检索类技能核实,再回答。"这两件事配合起来,模型乱答的比例下降得非常明显。

还有一个容易被忽略的原因:模型版本更新后,行为会变。同一个运行配置,上周还表现正常,这周模型升级后忽然不按套路调用工具了。所以生产环境里,给模型版本上锁是必要的,不要用"最新版"这样的模糊配置,避免行为漂移带来不可控的变化。

4.3 上下文爆炸与记忆管理

Agent循环跑得多了,上下文里会积累大量tool返回内容。很多时候一个数据库查询返回了几百行数据,全部塞进消息列表,几轮下来模型就"记不住"初始目标了。我在一次调试中发现,模型在长上下文里竟然忘记了用户最初的问题,开始顺着工具结果自由发挥。

解决这个问题的核心思路是"分层记忆"。我把记忆分成两层:短期工作记忆用于存放当前任务正在处理的数据,长期记忆用于存放任务目标、已完成步骤、关键结论。每次循环时,短期记忆只保留最新的工具结果摘要,长期记忆则持续更新。回传给模型时,把两层记忆合并成一段精炼的状态描述,而不是堆砌原始文本。

用摘要压缩长文本的时候,我习惯保留关键数字和名字,因为LLM在信息压缩中很容易丢失具体数值。我在摘要Prompt里特别强调:"保留所有数字和专有名词,可用省略句但不要模糊化关键数据。"这算是一个小细节,但能救回来好几次任务失败。

4.4 技能执行的安全与边界控制

最后聊一个跟技术关系不太大但同样重要的话题:执行安全。Agent一旦接入真实业务系统,它的操作就有真实的副作用。我在沙箱环境测试和真实环境部署之间,梳理了两条底线。

第一,高风险操作必须人工确认。比如删除数据、发送对外邮件、支付转账这类权重大、后果不可逆的执行,在Agent流程设计时就要插入"人类确认"节点。LLM可以生成操作指令,但真正提交前必须经人批准。这不算什么高级技术,但没了这一步,Agent上线后你随时可能被它的"自主发挥"背后捅一刀。

第二,给每个Skill划定执行边界。前面提到的注册表分区就是一种实现方式。更细化的手段包括:数据库查询禁止非SELECT语句、文件操作限制在指定目录、网络请求限定域名白名单。这些约束写死在Skill内部或注册表层面,不能被模型通过提示词绕过。我的原则是:宁可让Agent"显得笨"——不会做的操作直接说自己做不了,也不要让它去做可能造成伤害的操作。

我在测试中试着让Agent尝试突破边界,比如通过提示词注入,让它试图读取系统文件,好在一层一层的权限校验全部挡了回去。这套安全机制花的时间不少,但价值是被验证过的。生产环境里,你永远不知道用户会说出什么来。

4.5 常见问题速查表

为了方便排查,我把高频问题整理成一张速查表,遇到问题先对照一下:

现象常见原因排查思路实践解决方案
工具参数类型错误参数描述不够具体打印原始模型响应在参数schema中添加example字段
模型拒绝调用技能Skill描述与任务语义距离远检查触发条件描述补充近义触发词,增加反例
上下文过长导致遗忘工具结果重复塞入对话统计消息体长度增长实现分层记忆与摘要压缩
工具结果解析报错LLM返回格式不合预期打印异常JSON片段增加解析容错并重试一次
一条SQL导致死循环循环终止条件缺失检查最大轮次是否失效强制轮次上限 + 完成标志识别
技能之间的副作用Skill耦合度过高检查注册表依赖关系拆分为单一职责Skill

顺便说一个我在真实项目里遇到的"教训型问题":某次Agent一直重复调用同一个查询技能,原因是数据库返回的结果里包含一个错误值,导致模型认为查询没成功,于是一遍遍重试。最后定位到是数据本身有问题,而不是代码逻辑问题。只能说,模型的"错觉"也会传染给Agent。

5. 一些实操心得与后续扩展方向

写到这里,这篇学习笔记的核心内容差不多讲完了。最后分享几点我在大量试错之后沉淀下来的个人体会,算是一些没那么正式的心得。

第一件事,别迷信框架。很多人一提到Agent开发就想用现成的自动化框架,比如各类Auto-X框架。我试过一些,发现它们的问题在于"通用与场景不匹配"。框架帮你内置了很多通用的思考方式,但真实业务往往需要你精确控制调用过程。与其花大力气适配框架的抽象,不如自己写一个不到两百行的循环,再把Skill做精。到后面你回过头看,那些框架的核心也就是这个循环。

第二件事,把Skill当成产品来做。我之前总把Skill当工具函数看待,能用就行。后来慢慢意识到,一个优秀的Skill应该像一个设计良好的接口,它有清晰的用途、合理的错误提示、可预测的行为。如果团队里多个人合作开发Agent,把Skill文档写好,比把代码写漂亮更重要。我在自己的项目里,给每个Skill维护了一个Markdown文档,内容甚至包括"我什么时候不应该被调用"这种元信息。

第三件事,关于迭代思路。Agent开发跟传统软件开发很不一样的地方在于:它没有"完全正确"一说,只有"行为更稳定"。所以测试方式也要改变。我习惯准备一组固定的测试用例,每次调整提示词或Skill定义后,跑一遍看行为模式有没有偏离。这个方法虽然土,但在追求稳定性的场景下非常实用。

后续这个项目还可以往几个方向扩展。一是加入更完善的记忆机制,让跨会话的长期记忆真正落地——不是简单地存对话,而是把每次任务的结论沉淀成可以被检索的知识。二是引入更复杂的任务规划能力,比如让Agent自己拆解子任务并分配给多个子Agent执行,每个子Agent持有各自擅长的Skill。三是做更细粒度的可观测性,把每个决策步骤的推理依据和工具调用参数记录下来,方便离线复盘模型行为。这些方向我还在持续探索中,等有一些阶段性结果了,再来更新这份笔记。

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进