做后端和数据处理的朋友最近大概率都遇到过这样的场景:手里有个 AI 对话工具,问它几个问题觉得挺顺手,于是想让它帮忙把一批文档整理成一份汇总报告,结果任务丢进去,返回的要么是泛泛而谈的套话,要么是看似条理清晰、实际数据全是编的内容。于是很多人得出一个结论——这类工具只能聊天,办正事不行。
其实问题往往不在模型本身,而在于使用方式还停留在“问答”阶段。AI Agent(人工智能体)和普通对话式问答最大的区别,是它能自己拆解任务、调用工具、读取结果、修正方向。这篇文章就以 WorkBuddy 这类智能体产品为例,把从任务描述、工具配置、执行循环、结果校验到排错优化的完整使用流程讲一遍。文章面向已经用过对话式 AI、但还没系统用过 Agent 的开发者,也适合想把这套能力接进自己项目的后端同学。读完之后,你应该能独立完成一次“让 Agent 干活”的闭环,并且在换用其他智能体产品时,知道该迁移哪些东西、重写哪些东西。
1. 先把 AI Agent 这个概念说清楚
1.1 从“回答问题”到“完成任务”的三级跳
如果一定要给 AI Agent 一个通俗的定义,我会说它是“一个被允许动手的 AI”。普通对话式问答的链路很短:你提问,模型根据训练时学到的知识和上下文生成一段文字,结束。整个过程里,模型没有能力去读你的文件、查你的数据库、跑你的脚本,它只能“说”。
到了工具调用(Function Calling / Tool Use)这一步,模型多了一项能力:它在生成回答时,可以输出一个结构化的“我要调用某个工具,参数是这些”的请求,由外部程序真正执行,再把结果喂回给模型。这时候模型就从“只会说”变成了“会说也能让别人去做”。
而 Agent 是在工具调用之上又加了一层循环控制:它不是调一次工具就结束,而是可以“想一想 → 调工具 → 看结果 → 再想一想”,直到任务完成或者判定无法完成。这个循环,才是 Agent 和“带工具的问答”之间的本质差别。
所以我们现在常说的三级跳是:纯问答 → 单次工具调用 → 多轮自主循环。WorkBuddy 这类产品,以及市面上其他叫得上名字的智能体,主体能力都落在第三级,差别主要在工具生态、编排方式、界面和部署形态上。理解了这一点,你再看不同产品的文档,就会发现它们的核心概念高度重合。
1.2 Agent 的四个核心部件
不管哪个产品,拆开看基本都是四件东西在配合:
第一部分是任务理解与规划。用户给一句自然语言,Agent 要把它拆成若干可执行的子步骤。这个拆解能力决定了任务上限——一句话里包含五个动作,Agent 能拆出五步,任务就有戏;只理解成一步,后面必然失败。
第二部分是工具集(Tools)。工具就是 Agent 的手和脚,包括读文件、查数据库、发 HTTP 请求、执行命令、检索知识库等等。工具的粒度设计很关键:粒度太粗,Agent 不会用;粒度太细,一次任务要调几十次,慢且容易出错。
第三部分是记忆与上下文。Agent 在一个任务里要记住前面调了什么、拿到了什么。这部分通常是“短期窗口 + 外部存储”的组合:对话历史放在上下文窗口里,重要的中间结果落到文件或数据库里,需要时再检索回来。
第四部分是执行与校验。每轮工具调用后,要判断结果是否符合预期,不符合就调整策略。这一步是很多新手忽略的,也是 Agent 效果差异最大的地方。
你可以把这四部分想成一个刚入职的实习生:规划能力是他的理解力,工具集是他能用的软件和账号权限,记忆是他的笔记本,校验是他交付前的自查习惯。缺哪一样,活儿都干不利索。
1.3 为什么说“都差不多”
标题里那句“都差不多”,其实是有依据的。目前主流智能体产品的抽象层几乎一致:都有一个“指令/系统提示”入口,都有“工具/插件/技能”的注册机制,都有“知识库/文件”的挂载方式,都有“工作流/编排”的可视化配置,最后都落到一个“会话”里跑。
差异主要体现在三块:一是内置工具的质量和覆盖范围,比如有的产品自带联网检索,有的需要你自己接;二是编排能力,能不能把多个 Agent 串成流水线、能不能加人工确认节点;三是部署形态,是纯云端 SaaS,还是支持私有化。
这意味着,你今天在 WorkBuddy 里学会的一套方法——怎么描述任务、怎么定义工具、怎么设计校验、怎么看日志——换到另一个产品上,方法论可以整体搬过去,只需要替换掉具体的按钮位置和接口名。这也是我建议的学习路径:先把一套抽象吃透,再去对照具体产品。
1.4 什么样的任务适合交给 Agent
不是所有事情都值得上 Agent。判断标准我一般看三条:
第一,任务是否有明确的完成标准。像“整理这批合同里的关键条款,输出成表格”,完成标准清晰,适合。像“帮我想想产品下一步怎么做”,没有客观标准,交给 Agent 大概率得到一堆正确但无用的话。
第二,任务是否需要多步、且中间结果会影响下一步。如果需要读十个文件再汇总,适合;如果只是把一段文字翻译一下,普通对话就够了,上 Agent 属于杀鸡用牛刀。
第三,任务是否可验证。能被程序检查结果的任务(比如生成的表格行数对不对、字段是否齐全、JSON 能不能解析),最适合 Agent,因为你可以把校验做进流程里。不可验证的任务,一旦出错你很难发现。
反过来,涉及资金、权限变更、生产数据删除这类操作,即使技术上能做,也必须加人工确认环节。这一点后文还会展开。
2. 环境准备与工具清单
2.1 运行环境与版本说明
本文的示例代码使用 Python 编写,建议 3.10 及以上版本,主要是为了使用较新的类型注解语法(如list[str]、X | None),如果你用的是 3.8/3.9,把注解改成List[str]、Optional[X]即可,逻辑不受影响。具体版本号请以你所在团队的实际情况为准,本文重点演示配置思路,不绑定特定版本。
依赖方面,示例只用到标准库(json、pathlib、dataclasses、typing),目的是让你复制下来就能直接跑通,不被第三方包版本卡住。如果你要接入真实的大模型服务或者某个智能体平台的 SDK,再按官方文档安装对应的客户端包,安装时注意锁版本,避免出现“本地能跑、CI 挂掉”的情况。
操作系统上没有限制,Windows、macOS、Linux 都能运行示例。唯一要注意的是路径分隔符,示例里统一用pathlib.Path处理,避免手写\或/导致跨平台问题。
至于 WorkBuddy 这类智能体产品本身,注册账号、创建工作空间、拿到调用凭证这些步骤,各家的界面不太一样,按产品内的引导走即可。需要提醒的是,创建凭证之后一定要配置到环境变量或者本地配置文件里,不要直接写死在代码中提交到仓库,这一点后面会单独讲。
2.2 项目目录结构
为了避免后面代码找不到位置,先把目录结构定下来。这个结构不复杂,但每个文件职责清晰,方便你后续扩展:
agent-demo/ ├── agent_core.py # Agent 主循环与工具注册 ├── tools.py # 具体工具实现 ├── config.yaml # 配置:模型、路径、限制 ├── data/ │ └── input/ # 待处理的原始文本 ├── output/ # 生成的报告与中间结果 └── logs/ └── agent.log # 执行日志这个结构里,data/input放原料,output放产物,logs放执行轨迹,三者分开的好处是:出问题时你可以直接对比“输入什么、输出了什么、中间发生了什么”,排查效率会高很多。很多同学把中间结果和最终结果混在一个目录,出问题根本不知道哪个文件是哪一步产生的。
初始化命令很简单:
mkdir -p agent-demo/{data/input,output,logs} cd agent-demo touch agent_core.py tools.py config.yaml2.3 密钥与配置的分离管理
这一节看起来和 Agent 没什么关系,但实际项目里翻车最多的就是这里。常见错误写法是把 API Key 直接写进代码:
# 反面示例,不要这样写 API_KEY = "sk-xxxxxxxxxxxxxxxx"一旦这个文件进了 Git 历史,密钥就等于泄露了。正确做法是走环境变量,代码只读变量名:
# 文件路径:~/.bashrc 或项目的 .env(不要提交到仓库) export AGENT_API_KEY="your-key-here" export AGENT_BASE_URL="https://your-endpoint.example.com"# 文件路径:agent_core.py(片段) import os def load_credentials() -> dict[str, str]: key = os.getenv("AGENT_API_KEY") if not key: raise RuntimeError("缺少环境变量 AGENT_API_KEY,请先配置后再运行") return {"api_key": key, "base_url": os.getenv("AGENT_BASE_URL", "")}这里主动抛异常而不是给一个默认值,是有意为之。配置缺失就应该让程序在启动阶段直接失败,而不是带着空密钥跑一半再报一个看不懂的 401。快速失败(Fail Fast)在 Agent 场景里尤其重要,因为 Agent 的执行链路长,错误越晚暴露,定位成本越高。
3. Agent 的核心机制拆解
3.1 任务描述:把“我要什么”翻译成可执行指令
任务描述(很多人叫它 Prompt)是 Agent 效果的第一决定因素。新手最常见的写法是一句话:“帮我整理一下这些文档。”这句话对人来说都很模糊,对 Agent 更是没法执行。
一份合格的 Agent 任务描述,我一般要求包含五要素:目标、输入、输出格式、约束条件、验收标准。举个对比:
不合格的写法:
帮我分析这些客户反馈,写个报告。合格的写法:
目标:分析 data/input/ 目录下所有 .txt 客户反馈文件,统计高频问题类型。 输入:data/input/*.txt,每个文件是一段独立反馈,UTF-8 编码。 输出:生成 output/summary.md,包含三部分: 1) 问题类型统计表(类型 / 出现次数 / 占比),按次数降序; 2) 每类问题摘录 1 条原文,注明来源文件名; 3) 不超过 300 字的总述。 约束:只统计出现次数 >= 2 的类型;无法归类的放入"其他";不要编造未在原文中出现的描述。 验收:表格中所有"出现次数"之和等于归类成功的反馈总数。差别在哪?第二版把“什么叫整理好了”定义清楚了,Agent 才有可能自己判断有没有做完。而且“不要编造”这条约束很关键,它不能百分百杜绝幻觉,但能显著降低概率。
在实际使用 WorkBuddy 这类产品时,这类描述一般放在“系统提示/角色设定”里做长期约束,把本节的具体任务放在“用户消息”里做一次性指令。前者描述“你是谁、你的边界在哪”,后者描述“这次要干什么”,分开维护会让复用变得容易很多。
3.2 工具定义:让 Agent 知道能做什么、怎么传参
工具是 Agent 能力边界的物理体现。你给它什么工具,它就只能在这个范围里干活。工具定义一般用 JSON Schema 描述,核心是三段:名字、用途说明、参数结构。
{ "name": "read_text_file", "description": "读取指定路径的纯文本文件内容。仅支持 .txt 和 .md 文件,单文件不超过 1MB。", "parameters": { "type": "object", "properties": { "path": { "type": "string", "description": "相对于项目根目录的文件路径,例如 data/input/feedback_01.txt" } }, "required": ["path"] } }这里有三个容易踩的坑,值得单独说:
第一个坑是 description 写得太简单。很多人只写“读取文件”四个字。模型选择工具时高度依赖这段描述,描述太笼统会导致它选错工具或者传错参数。把限制条件(支持什么格式、大小上限、路径规则)写进去,命中率会明显提升。
第二个坑是工具太少或太粗。只给一个execute_shell工具,看起来万能,实际风险极高且效果不稳定,因为模型要自己拼命令,稍有不慎就是灾难。更好的做法是把高频操作封装成语义明确的原子工具,比如count_keyword_occurrences、list_files_by_extension。
第三个坑是工具太多。一口气注册三四十个工具,模型在选工具这一步的准确率会下降,还容易串。经验上,单个 Agent 控制在 10 个以内核心工具比较稳妥,其余按需动态加载。
3.3 记忆与上下文:短期窗口和长期存储怎么配合
Agent 干活的过程中会产生大量中间信息:读到的文件内容、统计结果、已经排除的方向。这些信息不能全塞进上下文窗口,一是长度有限,二是塞太多会稀释模型的注意力,导致它忘记最初的约束。
我的做法是分两层:
短期层放在对话历史里,只保留“决策相关信息”,比如“已处理 3/10 个文件,发现主要问题是响应速度”。原始文件内容用完就丢,不留在上下文里。
长期层落到磁盘,中间结果写成 JSON 或者 Markdown 文件,需要时通过工具再读回来。这样做还有个附带好处:任务中断后可以从中间结果续跑,不用从头再来。
一个很实用的小技巧是把“进度”显式记下来。比如维护一个output/progress.json:
{ "task_id": "feedback-2026-09-07", "total_files": 10, "processed_files": ["feedback_01.txt", "feedback_02.txt", "feedback_03.txt"], "status": "running", "updated_at": "2026-09-07T00:25:17" }当 Agent 上下文变长、开始“失忆”时,让它先读这个文件,就能快速恢复状态。这比单纯依赖上下文窗口可靠得多。
3.4 规划与反思:让 Agent 自己检查作业
反思(Reflection)是 Agent 从“能跑”到“跑得稳”的关键。具体做法是:在 Agent 认为任务完成、准备输出结果之前,插入一轮自检——把验收标准重新给它,让它检查自己的产出是否满足。
自检的问题可以写得很具体:
请检查以下三点,逐条回答"是"或"否",若为"否"请说明原因: 1. 输出文件是否存在于 output/summary.md? 2. 表格中"出现次数"列的总和,是否等于归类成功的反馈总数? 3. 每个问题类型是否都至少引用了 1 条原文及来源文件名?这种“结构化自检”比让模型自己“反思一下”有效得多,因为它把模糊的判断变成了可执行的检查项。更进一步,能让程序验证的项就不要交给模型验证——比如文件是否存在、JSON 能否解析、数字加和是否相等,直接用代码校验,既快又准。
4. 完整实战:让 Agent 完成一次文档汇总任务
前面讲的是原理,这一节把它跑起来。任务设定为:读取data/input/下所有文本文件,统计高频关键词,生成一份 Markdown 汇总报告,并在生成后做一次程序化校验。
为了让你不依赖任何外部服务就能跑通,示例里的“模型决策”部分用一个mock_llm函数模拟,它返回固定的决策序列。等你把这套骨架跑通,再把mock_llm换成真实模型调用或者智能体平台的 SDK,其余代码基本不用动。
4.1 目录与示例数据准备
先造一点测试数据,方便观察结果:
# 文件路径:项目根目录下执行 cat > data/input/feedback_01.txt <<'EOF' 页面加载速度太慢了,尤其是报表页面,打开要等十几秒。 EOF cat > data/input/feedback_02.txt <<'EOF' 导出功能很好用,但是导出速度慢,希望能优化。 EOF cat > data/input/feedback_03.txt <<'EOF' 登录经常失败,提示密码错误,但我确认密码没问题。 EOF三个文件覆盖了“速度慢”和“登录失败”两类问题,后面统计时能直观看出结果对不对。
4.2 工具实现:把能力封装成函数
工具实现放在tools.py,每个函数只做一件事,参数和返回值都保持简单类型,方便序列化。
# 文件路径:tools.py from __future__ import annotations import json import re from pathlib import Path from collections import Counter BASE_DIR = Path(__file__).resolve().parent def list_input_files(ext: str = ".txt") -> list[str]: """列出 data/input 下指定后缀的文件,返回相对路径。""" input_dir = BASE_DIR / "data" / "input" if not input_dir.exists(): return [] return sorted( str(p.relative_to(BASE_DIR)) for p in input_dir.iterdir() if p.is_file() and p.suffix.lower() == ext ) def read_text_file(path: str) -> str: """读取项目内文本文件,路径做了越权校验。""" target = (BASE_DIR / path).resolve() # 安全检查:禁止读取项目目录之外的文件 if not str(target).startswith(str(BASE_DIR)): raise ValueError(f"非法路径,禁止访问项目目录之外的文件:{path}") if not target.exists(): raise FileNotFoundError(f"文件不存在:{path}") if target.stat().st_size > 1024 * 1024: raise ValueError(f"文件超过 1MB 限制:{path}") return target.read_text(encoding="utf-8") def count_keywords(texts: dict[str, str], keywords: list[str]) -> dict[str, int]: """统计关键词在全部文本中出现的次数。""" counter: Counter[str] = Counter() for content in texts.values(): for kw in keywords: counter[kw] += len(re.findall(re.escape(kw), content)) return {kw: counter[kw] for kw in keywords} def write_report(path: str, content: str) -> dict[str, str]: """写出报告文件,返回写入结果。""" target = (BASE_DIR / path).resolve() if not str(target).startswith(str(BASE_DIR)): raise ValueError(f"非法输出路径:{path}") target.parent.mkdir(parents=True, exist_ok=True) target.write_text(content, encoding="utf-8") return {"status": "ok", "path": path, "bytes": str(target.stat().st_size)}这段代码里有三个细节值得强调。
第一,read_text_file里的路径越权校验。Agent 生成的路径参数来自模型输出,模型有可能拼出../../etc/passwd这种路径。虽然大部分情况下是模型“手滑”,但作为工程代码,必须假设输入不可信,在工具层做兜底。这就是最小权限原则的落地方式。
第二,文件大小限制。Agent 很容易无脑读取大文件,把上下文撑爆。在工具层加一道大小限制,比在提示词里写“不要读大文件”可靠一百倍。
第三,返回值统一用可 JSON 序列化的类型(字符串、数字、字典)。Agent 的循环里,工具结果要回传给模型,不能序列化会直接崩掉。
4.3 工具注册表与 JSON Schema
工具实现好了,还要告诉模型“有哪些工具、怎么用”。注册表负责建立“名字 → 函数 + Schema”的映射。
# 文件路径:agent_core.py(工具注册部分) from __future__ import annotations import json from dataclasses import dataclass from typing import Any, Callable import tools @dataclass class ToolSpec: name: str description: str parameters: dict[str, Any] func: Callable[..., Any] def to_schema(self) -> dict[str, Any]: return { "name": self.name, "description": self.description, "parameters": self.parameters, } TOOL_REGISTRY: dict[str, ToolSpec] = {} def register_tool(spec: ToolSpec) -> None: if spec.name in TOOL_REGISTRY: raise ValueError(f"工具重复注册:{spec.name}") TOOL_REGISTRY[spec.name] = spec register_tool(ToolSpec( name="list_input_files", description="列出 data/input 目录下指定后缀的文件,返回相对路径列表。", parameters={ "type": "object", "properties": { "ext": {"type": "string", "description": "文件后缀,默认 .txt"} }, "required": [], }, func=tools.list_input_files, )) register_tool(ToolSpec( name="read_text_file", description="读取项目内文本文件内容,单文件不超过 1MB,禁止访问项目目录之外。", parameters={ "type": "object", "properties": { "path": {"type": "string", "description": "相对项目根目录的文件路径"} }, "required": ["path"], }, func=tools.read_text_file, )) register_tool(ToolSpec( name="count_keywords", description="统计关键词在给定文本集合中出现的总次数。", parameters={ "type": "object", "properties": { "texts": {"type": "object", "description": "文件名到文本内容的映射"}, "keywords": {"type": "array", "items": {"type": "string"}}, }, "required": ["texts", "keywords"], }, func=tools.count_keywords, )) register_tool(ToolSpec( name="write_report", description="将最终报告写入指定路径,路径需在项目目录内。", parameters={ "type": "object", "properties": { "path": {"type": "string", "description": "输出文件相对路径"}, "content": {"type": "string", "description": "报告内容,Markdown 格式"}, }, "required": ["path", "content"], }, func=tools.write_report, )) def get_tool_schemas() -> list[dict[str, Any]]: return [spec.to_schema() for spec in TOOL_REGISTRY.values()] def call_tool(name: str, arguments: dict[str, Any]) -> dict[str, Any]: """统一工具调用入口,捕获异常并返回结构化结果。""" spec = TOOL_REGISTRY.get(name) if spec is None: return {"ok": False, "error": f"未注册的工具:{name}"} try: result = spec.func(**arguments) return {"ok": True, "result": result} except Exception as exc: # 工具异常不抛出,转为结构化错误回传给模型 return {"ok": False, "error": f"{type(exc).__name__}: {exc}"}注意call_tool里的异常处理策略:工具抛出的异常不向上冒泡,而是转成{"ok": false, "error": "..."}返回给模型。这一点很关键——把错误信息喂回去,模型才有机会自我修正,比如路径写错了就换个路径重试。如果直接让程序崩溃,整个任务就中断了。
4.4 Agent 主循环实现
主循环负责三件事:拿决策、执行工具、判断是否结束。这里用mock_llm模拟模型决策,返回值结构和真实模型的工具调用格式保持同构,方便后续替换。
# 文件路径:agent_core.py(主循环部分,接在上面的注册代码之后) import time from pathlib import Path BASE_DIR = Path(__file__).resolve().parent def mock_llm(step: int, history: list[dict[str, Any]]) -> dict[str, Any]: """模拟模型决策,返回下一步动作。真实场景替换为模型/SDK 调用。""" if step == 0: return {"type": "tool_call", "tool": "list_input_files", "arguments": {"ext": ".txt"}} if step == 1: return {"type": "tool_call", "tool": "read_text_file", "arguments": {"path": "data/input/feedback_01.txt"}} if step == 2: return {"type": "tool_call", "tool": "read_text_file", "arguments": {"path": "data/input/feedback_02.txt"}} if step == 3: return {"type": "tool_call", "tool": "read_text_file", "arguments": {"path": "data/input/feedback_03.txt"}} if step == 4: return {"type": "tool_call", "tool": "count_keywords", "arguments": {"texts": "PENDING", "keywords": ["慢", "失败", "速度"]}} if step == 5: return {"type": "tool_call", "tool": "write_report", "arguments": {"path": "output/summary.md", "content": "PENDING"}} return {"type": "final", "message": "任务完成"} def build_report(counts: dict[str, int], total_files: int) -> str: lines = ["# 客户反馈关键词汇总", "", f"共处理文件数:{total_files}", "", "| 关键词 | 出现次数 |", "| --- | --- |"] for kw, cnt in sorted(counts.items(), key=lambda x: -x[1]): lines.append(f"| {kw} | {cnt} |") lines.append("") lines.append("> 本报告由 Agent 自动生成,数据来源于 data/input 目录。") return "\n".join(lines) def run_agent(max_steps: int = 12) -> dict[str, Any]: history: list[dict[str, Any]] = [] memory: dict[str, Any] = {"files": [], "texts": {}, "counts": {}} log_path = BASE_DIR / "logs" / "agent.log" log_path.parent.mkdir(parents=True, exist_ok=True) for step in range(max_steps): decision = mock_llm(step, history) _log(log_path, step, decision) if decision["type"] == "final": return {"ok": True, "steps": step + 1, "memory": memory} name = decision["tool"] args = dict(decision["arguments"]) # 关键词统计需要依赖前面读到的文本,做一次参数填充 if name == "count_keywords" and args.get("texts") == "PENDING": args["texts"] = memory["texts"] # 报告内容需要依赖统计结果 if name == "write_report" and args.get("content") == "PENDING": args["content"] = build_report(memory["counts"], len(memory["texts"])) result = call_tool(name, args) history.append({"step": step, "tool": name, "arguments": args, "result": result}) if result["ok"]: if name == "count_keywords": memory["counts"] = result["result"] elif name == "list_input_files": memory["files"] = result["result"] elif name == "read_text_file": path = args["path"] memory["texts"][path] = result["result"] return {"ok": False, "error": f"超过最大步数 {max_steps},任务未完成"} def _log(log_path: Path, step: int, payload: dict[str, Any]) -> None: record = {"ts": time.strftime("%Y-%m-%d %H:%M:%S"), "step": step, "payload": payload} with log_path.open("a", encoding="utf-8") as f: f.write(json.dumps(record, ensure_ascii=False) + "\n") if __name__ == "__main__": outcome = run_agent() print(json.dumps( {k: v for k, v in outcome.items() if k != "memory"}, ensure_ascii=False, indent=2 ))插一句说明:mock_llm只是为了让示例可以离线运行。在真实项目里,你要把mock_llm替换成对模型或智能体平台 SDK 的调用,把get_tool_schemas()的结果作为工具定义传给对方,再把返回的tool_calls解析成上面的decision结构。中间的循环控制、参数填充、日志记录都不需要改。这也正是前面说的“抽象层一致”带来的好处。
4.5 运行与结果验证
执行命令:
python agent_core.py预期输出类似:
{ "ok": true, "steps": 6 }再检查产物:
cat output/summary.md应该能看到一张关键词统计表。同时logs/agent.log里会有每一步的决策记录,格式是每行一条 JSON,方便直接丢进日志系统分析。
到这里,一个最简的 Agent 闭环就跑通了。但它还只是“能跑”,离“跑得稳”还有距离。真正上项目时,你需要补上这几块:
第一,输出校验。写一个独立脚本检查报告结构是否符合验收标准,比如表格行数、数值加和、必填字段。校验不通过就让 Agent 重做,而不是直接交付。
# 文件路径:validate.py from __future__ import annotations import re import sys from pathlib import Path BASE_DIR = Path(__file__).resolve().parent def validate_summary(path: str = "output/summary.md") -> tuple[bool, list[str]]: target = BASE_DIR / path errors: list[str] = [] if not target.exists(): return False, [f"输出文件不存在:{path}"] content = target.read_text(encoding="utf-8") if "| 关键词 | 出现次数 |" not in content: errors.append("缺少关键词统计表头") rows = re.findall(r"^\|\s*(.+?)\s*\|\s*(\d+)\s*\|$", content, flags=re.MULTILINE) if not rows: errors.append("统计表没有有效数据行") for kw, cnt in rows: if kw in ("关键词", "---"): continue if int(cnt) < 0: errors.append(f"关键词 {kw} 的计数为负数") if "自动生成" not in content: errors.append("缺少来源说明") return len(errors) == 0, errors if __name__ == "__main__": ok, errs = validate_summary() if ok: print("校验通过") sys.exit(0) print("校验失败:") for e in errs: print(" -", e) sys.exit(1)第二,最大步数与超时。上面用了max_steps,实际还要加整体超时。没有刹车机制的 Agent 是真能在循环里转很久的,每一步都是成本。
第三,人工确认节点。如果任务是“发邮件”“写数据库”“改配置”,务必在关键动作前插入确认。技术上可以让 Agent 停下来等一个信号,或者干脆把这类动作拆成“生成草稿”和“执行提交”两个任务。
第四,中间结果落盘。当前示例把中间结果放在内存的memory变量里,进程一挂就全没了。生产场景建议每步写一次output/progress.json,支持断点续跑。
5. 换一个智能体平台怎么办
5.1 一张对照表搞定能力映射
学完 WorkBuddy,很多人的下一个问题是:公司用的是另一个产品,怎么迁移?答案是把能力拆成通用维度,逐个对照。下面这张表可以帮助你快速定位差异:
| 能力维度 | 你需要确认的事 | 迁移动作 |
|---|---|---|
| 指令入口 | 系统提示在哪配、是否支持模板变量 | 把五要素任务描述拆成系统约束 + 用户指令 |
| 工具注册 | 支持 JSON Schema 还是表单配置 | 直接复用 Schema,字段名按平台要求调整 |
| 知识库 | 支持的格式、切片方式、检索策略 | 重新导入,验证召回效果 |
| 编排能力 | 是否支持多步骤、条件分支、循环 | 把主循环逻辑改成可视化节点或代码 |
| 记忆 | 会话内记忆、跨会话持久化 | 关键中间结果仍建议自己落盘 |
| 日志 | 是否提供执行轨迹、能否导出 | 自己的日志体系不要省,双写更稳妥 |
这张表的价值在于,它让你在拿到一个新产品时,知道花半小时先确认哪六件事,而不是一头扎进教程里从头学一遍。
5.2 提示词和工具的最小迁移集
迁移的时候不用把所有东西都搬,先搬“最小可用集”:一份系统提示、三个核心工具、一套输出校验。这三样搞定,80% 的常规任务就能跑起来。
系统提示建议写成独立文件维护,不要散落在各处:
# 文件路径:config.yaml agent: name: "feedback-analyzer" max_steps: 12 timeout_seconds: 300 system_prompt: | 你是一个客户反馈分析助手。 你的工作目录是项目根目录,只能读取 data/input 下的文件,只能写入 output 目录。 你必须在输出结果前自检:统计表的所有计数之和是否等于归类成功的反馈总数。 如果信息不足,请明确说明缺少什么,不要编造内容。 allowed_paths: read: ["data/input"] write: ["output", "logs"] tools: enabled: - list_input_files - read_text_file - count_keywords - write_report这份配置里,allowed_paths是安全边界,system_prompt是行为约束,tools.enabled是能力白名单。三者配合,能挡掉大部分意外操作。
5.3 从“会用某一个”到“会用一个品类”
我自己总结的规律是:智能体产品的学习成本,前期看界面,中期看工具生态,后期看编排能力。所以迁移时,最不值钱的是界面操作,最值钱的是你那套任务拆解和校验方法。
具体练法建议是:同一个任务,用两个不同产品各做一遍。第一遍你会关注“按钮在哪”,第二遍你才会真正关注“为什么这个任务拆成五步比拆成三步效果好”。这种横向对比做上三五次,你对 Agent 的理解就会从“会用工具”变成“会设计流程”。
6. 常见问题与排查思路
6.1 高频问题速查表
| 问题现象 | 常见原因 | 解决思路 |
|---|---|---|
| Agent 不调用工具,直接给一段文字回答 | 工具描述太模糊,或任务描述里没暗示需要读数据 | 补全工具 description,在任务里明确写出输入文件路径 |
| 反复调用同一个工具、同一参数 | 缺少状态记录,模型不知道已经做过 | 在上下文里显式写"已完成 X,当前进度 Y",或落盘 progress |
| 输出内容包含原文里没有的信息 | 提示词没有约束,或输入过长导致模型"补充" | 加"不得编造"约束,压缩输入,增加可验证的引用要求 |
| 工具报错后 Agent 直接终止 | 工具异常向上抛出,未转成结构化错误 | 统一用call_tool包装,异常转ok: false回传 |
| 任务跑到一半超时 | 步数无上限、单步耗时过长 | 设置max_steps和整体超时,拆细任务 |
| 报告数字和原文对不上 | 缺少程序化校验,全靠模型自检 | 增加validate.py之类的独立校验脚本 |
| 换环境后读不到文件 | 使用了绝对路径或相对路径基准不一致 | 统一用Path(__file__).resolve().parent作为基准 |
| 上下文越来越长、回答越来越飘 | 中间结果全塞进了上下文 | 中间结果落盘,上下文只保留决策摘要 |
6.2 三个典型故障的排查过程
故障一:Agent 只聊天不动手。现象是你让它分析文件,它回了一段“根据您提供的文件内容……”的分析,但日志里一次工具调用都没有。排查顺序是:先看工具是否真的注册成功(打印get_tool_schemas()的长度);再看工具描述是否能让人看懂用途;最后看任务描述里有没有给出明确的输入路径。这三步里,第二步出问题的概率最高——描述写得太泛,模型判断“不需要调工具也能回答”。
故障二:循环停不下来。现象是步数蹭蹭往上涨,每步都在读同一个文件。根因通常是状态没有反馈给模型。解决办法是在每轮把“已完成动作摘要”拼进上下文,一句话就够:“已读取 feedback_01.txt 至 feedback_03.txt,共 3 个文件,下一步应执行关键词统计。”再加一个max_steps兜底。
故障三:校验通过但结果不对。这个最隐蔽。校验脚本只检查了格式,没检查语义。比如关键词统计表格式完全正确,但统计的关键词压根不是用户关心的。这类问题的解法是把验收标准写得更贴近业务:不只是“表格存在”,而是“表格必须包含用户指定的关键词列表”。校验项越贴近业务语义,越能发现问题。
7. 最佳实践与工程建议
7.1 权限边界要写在代码里,不只写在提示词里
这是我最想强调的一条。提示词里写“不要访问其他目录”,只是建议;工具层做路径校验,才是约束。前文的read_text_file里那段startswith检查,看起来简陋,但它挡住的是真实存在的风险。
更完整的做法包括:读写目录分离(读只允许data/input,写只允许output)、危险操作二次确认、凭证永远不进入模型上下文、数据库账号只给最小权限。如果你要把 Agent 接到生产系统,这几条不是可选项。
7.2 成本与性能的平衡
Agent 的成本主要来自三块:模型调用次数、上下文长度、工具执行时间。优化顺序建议是先降次数,再压长度,最后优化工具。
降次数靠拆任务和加缓存。同一个文件被读三次,说明状态管理有问题。压长度靠中间结果落盘,别把全文塞进上下文。优化工具则是把慢操作批量化,比如十个文件一次读,而不是分十次。
还有一个容易被忽视的点:不是所有步骤都需要模型参与。像“统计关键词出现次数”这种确定性计算,完全可以用代码做,只把结果给模型。把确定性工作交给代码、把判断类工作交给模型,是控制成本最有效的思路。
7.3 日志与可观测性
Agent 出问题时,你唯一能依靠的就是日志。建议至少记录四类信息:每步的决策(调了哪个工具、什么参数)、工具返回(成功还是失败、错误信息)、耗时、以及当时的上下文长度。
日志格式优先选结构化 JSON,一行一条,方便后续用jq或者日志平台分析:
# 查看所有失败的工具调用 jq -c 'select(.payload.type == "tool_call")' logs/agent.log | head -20日志里不要记录密钥、完整用户隐私数据。这一点在接入真实业务数据时必须提前想清楚,事后脱敏的成本要高得多。
7.4 团队协作与资产沉淀
一个人用 Agent,效率提升有限;一个团队把 Agent 用起来,关键在资产沉淀。我建议团队维护三份东西:
第一份是任务模板库,把验证过好用的任务描述存下来,新人直接改参数就能用。第二份是工具库,公共能力(读文件、查数据库、发通知)统一封装,避免每个人重复造。第三份是踩坑记录,哪个工具描述改过之后命中率提升了,哪个任务必须加校验,都记下来。
这三份东西积累三个月,团队用 Agent 的整体成功率会有明显变化。因为它们解决的是“经验无法传递”的问题,而这恰恰是这类工具落地时最大的隐性成本。
8. 学习路线与下一步
回头看这篇文章覆盖的内容:Agent 的四部件模型(规划、工具、记忆、校验)、任务描述的五要素(目标、输入、输出格式、约束、验收标准)、工具定义的三段式 Schema、主循环的异常处理策略、以及权限边界和日志这些工程细节。这些不是某个产品特有的,而是这一类工具的共同底座。
如果你想继续深入,我建议按这个顺序走:先用本文的示例把闭环跑通,哪怕用 mock 也要跑一遍,因为只有跑起来你才会真正理解循环控制;然后把mock_llm换成真实的模型或平台 SDK,感受一下真实决策的不确定性;接着给自己找一个真实的小任务——比如整理一周的会议纪要、汇总一批工单——完整做一遍,重点练校验部分;最后再去看那些进阶能力,比如多 Agent 协作、可视化编排、RAG 检索增强。
实际项目里,优先级最高的是三件事:权限边界、输出校验、成本控制。这三件做好了,Agent 就算不上惊艳但足够可靠;这三件做不好,效果再好也不敢放到生产上。至于选哪个产品,说实话在方法层面差别真的不大,把一套抽象吃透,换平台的成本比你想象的低得多。
如果你手头正好有个重复性高的文本处理任务,不妨今天就拿它练手,把它拆成五要素写出来,配上三四个工具,跑一轮看看。跑通一次之后,你会发现后面很多任务都是同一个套路的变形。