简介:这份《字节跳动 Agent 实践手册》面向具备一定技术背景的产品经理、AI开发者、技术管理者及企业数字化转型负责人,尤其适合从事智能系统设计、大模型应用开发与业务创新的1-3年经验从业者。内容系统梳理了字节跳动在Agent领域的实践路径与全景布局,从感知层、推理层、执行层的分层架构,到大语言模型、工具调用、多模态融合等核心技术组件,再到需求分析、模型配置、插件集成、开发测试的完整流程均有覆盖。手册结合飞书智能办公集群、抖音电商智能运营等典型案例,深入讲解办公、电商、内容创作、教育等多元场景的落地方法,并围绕运营数据监测、性能优化、用户体验提升、安全合规与全球化适配展开论述,同时给出混合决策模式、数据闭环迭代、合规前置等关键策略。资源为1个PDF文件,压缩包约3.37MB,目录结构清晰,涵盖引言、技术基础、开发流程、应用场景、运营优化、安全合规、未来方向、案例剖析、团队协作与风险应对等模块。目前已有639人学习,适合对照工具包模板分阶段实践,以解决业务痛点为核心目标。
1. 字节跳动 Agent 实践手册:从单点工具到多 Agent 协作的落地路线
很多团队做 Agent 的第一反应是接一个大模型 API,套个 ReAct 循环,能查天气、能算数学题就算完事。但真把它扔进业务流里,问题立刻暴露:工具调用不稳定、上下文越滚越长、多轮任务中途跑偏、多个 Agent 互相覆盖对方的修改。字节跳动内部在 Agent 方向的实践,核心不是「怎么让模型更聪明」,而是「怎么让 Agent 在工程上可控」。这本实践手册要解决的,就是 Agent 从 demo 到生产之间的那段路:任务怎么拆、记忆怎么分层、工具怎么编排、多 Agent 怎么协作、评测怎么做。适合已经跑通过一个最小 Agent、但卡在稳定性与规模化上的开发者,也适合正在做 agent 开发学习路线选型的技术负责人。下面按「先立住架构、再动手复现、最后避坑」的顺序展开。
2. Agent 架构选型:ReAct、Plan-and-Execute 与多 Agent 协作怎么选
2.1 三种主流 agent 框架的边界与适用场景
Agent 架构没有银弹,选错了后面全是补丁。目前工程上真正跑得住的,基本落在三类:ReAct 循环、Plan-and-Execute、多 Agent 协作。ReAct 是最小可用单元,模型每一步都做「思考→行动→观察」,适合工具少、任务短、容错高的场景,比如内部知识库问答、单步数据查询。它的硬伤是长任务容易陷入循环,因为模型没有全局计划,走一步看一步。
Plan-and-Execute 先把任务拆成步骤列表,再逐步执行,适合步骤明确、可预先分解的任务,比如「拉取数据→清洗→生成报表→发送」。它的风险在计划本身可能错,一旦第一步拆错,后面全歪,所以需要计划校验环节。多 Agent 协作则是把不同职责交给不同 Agent,比如一个负责检索、一个负责写代码、一个负责审查,适合复杂且需要交叉验证的任务。代价是通信成本和状态同步复杂度陡增。
选型时我一般看三个维度:任务步数是否超过 5 步、是否需要外部工具超过 3 个、是否要求结果可审计。三个都「是」,才考虑多 Agent;否则单 Agent 加好的工具编排就够了。很多团队一上来就上多 Agent,结果调试成本翻倍,这是血泪经验。
2.2 用 Python 搭一个可扩展的 Agent 骨架
下面这个骨架把「规划器 + 执行器 + 工具注册」拆开,方便后续替换任意一环。它不是某个官方实现,而是我按常见做法整理的最小可运行结构。
import json from typing import Callable class ToolRegistry: """工具注册表:所有 Agent 可调用的能力都从这里取""" def __init__(self): self._tools = {} def register(self, name: str, fn: Callable, desc: str): self._tools[name] = {"fn": fn, "desc": desc} def call(self, name: str, **kwargs): if name not in self._tools: raise ValueError(f"未注册的工具: {name}") return self._tools[name]["fn"](**kwargs) def describe(self): # 把工具描述拼进 prompt,让模型知道有哪些能力 return "\n".join(f"- {n}: {t['desc']}" for n, t in self._tools.items()) class Planner: """规划器:把用户目标拆成步骤列表""" def __init__(self, llm_client): self.llm = llm_client def plan(self, goal: str, tool_desc: str) -> list: prompt = f"目标: {goal}\n可用工具:\n{tool_desc}\n请输出 JSON 步骤数组" raw = self.llm.chat(prompt) return json.loads(raw) # 实际要加异常兜底 class Executor: """执行器:按步骤调用工具,记录中间结果""" def __init__(self, registry: ToolRegistry): self.registry = registry self.history = [] def run(self, steps: list): for step in steps: tool = step.get("tool") args = step.get("args", {}) result = self.registry.call(tool, **args) self.history.append({"step": step, "result": result}) return self.history逻辑说明:ToolRegistry把工具和描述集中管理,避免工具散落在各处导致模型「不知道能干什么」。Planner只负责拆步骤,不负责执行,这样计划错了可以单独重跑。Executor按步骤调用并记录历史,历史就是后续做记忆压缩和审计的原料。
参数说明:llm_client需要支持chat方法,返回字符串;plan里对 JSON 解析必须加 try/except,模型输出格式不稳定是常态。工具注册时desc要写清楚输入输出,否则模型调用参数经常错。
2.3 多 Agent 协作的通信与状态同步
多 Agent 最容易翻车的地方不是模型能力,而是状态同步。常见做法是设一个共享的「黑板」结构,所有 Agent 读写同一份任务状态,而不是互相直接传消息。直接传消息会导致消息顺序错乱、重复处理。
class Blackboard: """共享状态:所有 Agent 读写同一份任务上下文""" def __init__(self): self.state = {"facts": [], "artifacts": {}, "status": "running"} def add_fact(self, fact: str, source: str): self.state["facts"].append({"fact": fact, "source": source}) def set_artifact(self, key: str, value): self.state["artifacts"][key] = value def snapshot(self): # 给每个 Agent 看的状态快照,避免读到写一半的数据 return json.loads(json.dumps(self.state))逻辑说明:add_fact记录事实来源,方便追溯是哪个 Agent 得出的结论;set_artifact存中间产物,比如生成的代码、检索到的文档;snapshot做深拷贝,防止一个 Agent 读到另一个 Agent 写了一半的状态。
参数说明:facts列表会随任务增长,必须配合记忆压缩策略,否则上下文很快超限。status用于标记任务是否被中断,多 Agent 场景下要有一个协调者负责改这个字段。
3. Agent 记忆体系:短期、长期、永久记忆的分层实现
3.1 三层记忆的职责划分与存储选型
Agent 记忆不是「把历史全塞进 prompt」。字节跳动在 Agent 记忆上的实践思路是分层:短期记忆放当前会话的最近几轮,长期记忆放跨会话的事实和偏好,永久记忆放稳定不变的知识。短期记忆用内存队列,长期记忆用向量库,永久记忆用结构化存储或知识库。
选型上,短期记忆用collections.deque限制长度即可;长期记忆常见做法是向量数据库,比如 FAISS、Milvus,按语义检索;永久记忆用关系库或 KV,按 key 精确查。三层混用是常态,关键是每层有明确的写入和淘汰策略。
3.2 记忆写入与检索的最小实现
from collections import deque import hashlib class MemoryManager: def __init__(self, vector_store, kv_store, short_limit=10): self.short = deque(maxlen=short_limit) # 短期:固定长度 self.vector = vector_store # 长期:语义检索 self.kv = kv_store # 永久:精确查 def add_short(self, turn: dict): self.short.append(turn) def add_long(self, text: str, meta: dict): # 长期记忆写入前做去重,避免重复事实堆积 key = hashlib.md5(text.encode()).hexdigest() if not self.vector.exists(key): self.vector.add(key, text, meta) def add_permanent(self, key: str, value): self.kv.set(key, value) def recall(self, query: str, top_k=3): # 短期全带,长期按语义取 top_k,永久按需查 short_ctx = list(self.short) long_ctx = self.vector.search(query, top_k) return {"short": short_ctx, "long": long_ctx}逻辑说明:add_long用 md5 去重,防止同一事实反复写入导致检索结果冗余。recall把短期和长期拼在一起返回,永久记忆不主动拼,因为它是精确查,按需调用。
参数说明:short_limit一般设 8 到 12,太大浪费上下文,太小丢关键信息。top_k设 3 到 5,太多会引入噪声。向量库的exists方法需要自己封装,不是所有库都原生支持。
3.3 记忆压缩:什么时候该丢、什么时候该留
记忆压缩的触发条件通常是 token 数超过阈值。常见做法是保留最近 N 轮原文,更早的做摘要。摘要不是随便让模型写,而是按「事实、决策、未完成项」三类抽取。
def compress(history: list, llm_client, keep_recent=5): if len(history) <= keep_recent: return history old = history[:-keep_recent] recent = history[-keep_recent:] prompt = "把以下对话压缩成三类:事实、决策、未完成项\n" + json.dumps(old) summary = llm_client.chat(prompt) return [{"type": "summary", "content": summary}] + recent逻辑说明:keep_recent保证最近几轮原文不丢,因为最近的信息最可能被引用。摘要按三类抽取,是为了后续检索时能按类型过滤。
参数说明:keep_recent一般 4 到 6。摘要 prompt 要固定格式,否则每次输出结构不一致,后续解析会翻车。
4. Agent 工具编排与执行:从函数注册到失败重试
4.1 工具描述怎么写模型才不调错
工具调错,八成是描述没写清楚。常见做法是每个工具描述包含三部分:用途、输入参数及类型、返回结构。不要写「查询数据」这种模糊描述,要写「根据用户 ID 查询订单列表,输入 user_id 字符串,返回订单数组,每项含 order_id 和 amount」。
registry.register( name="query_orders", fn=query_orders, desc="根据用户ID查询订单。输入: user_id(str)。返回: [{order_id, amount}]" )逻辑说明:描述里带输入输出结构,模型在生成参数时错误率明显下降。参数类型写清楚,能减少字符串和数字混用的问题。
参数说明:desc控制在 50 字以内,太长会挤占上下文。多个工具描述之间用换行分隔,方便模型区分。
4.2 执行失败的重试与降级策略
工具调用失败是常态,网络超时、参数错、返回空都要处理。常见做法是分三类:可重试错误(超时、限流)重试 2 到 3 次,指数退避;参数错误不重试,把错误信息回传给模型让它改参数;业务错误(比如查无数据)直接返回空,让模型决定下一步。
import time def call_with_retry(registry, name, retries=3, **kwargs): for i in range(retries): try: return registry.call(name, **kwargs) except TimeoutError: time.sleep(2 ** i) # 指数退避 except ValueError as e: # 参数错误,直接回传,不重试 return {"error": str(e), "retryable": False} return {"error": "重试耗尽", "retryable": False}逻辑说明:TimeoutError走重试,ValueError直接返回,因为参数错重试也没用。返回结构里带retryable字段,方便上层决定是否让模型重新规划。
参数说明:retries设 2 到 3,太多会拖长任务时间。退避基数 2 秒起步,避免瞬间打爆下游。
4.3 工具调用的可观测性:日志与追踪
Agent 出问题时,最怕没有日志。常见做法是每次工具调用记录:调用时间、工具名、参数、返回、耗时、是否重试。这些日志按任务 ID 串起来,方便回放。
import logging, time def traced_call(registry, name, task_id, **kwargs): start = time.time() try: result = registry.call(name, **kwargs) status = "ok" except Exception as e: result = {"error": str(e)} status = "error" logging.info({ "task_id": task_id, "tool": name, "args": kwargs, "result": result, "status": status, "cost_ms": int((time.time() - start) * 1000) }) return result逻辑说明:task_id是串联所有调用的关键,没有它日志就是散的。cost_ms用于发现慢工具,慢工具往往是任务超时的元凶。
参数说明:日志用结构化格式,方便后续用 ELK 或类似系统检索。args里如果有敏感字段,要脱敏后再记。
5. Agent 评测与避坑:上线前必须过的五道坎
5.1 评测集怎么建才有区分度
Agent 评测不能只用「能不能答对」,要分维度:任务完成率、工具调用准确率、平均步数、失败恢复率。评测集要包含正常任务、边界任务、故意设错的任务。正常任务占 60%,边界 30%,设错 10%。设错任务用来测 Agent 能不能识别并纠正。
5.2 五个高频踩坑记录
坑一:上下文越滚越长导致模型「失忆」。现象是任务跑到后面,模型忘了前面的约束。原因是历史全塞进 prompt,超出有效窗口。解决是按第 3 章的记忆压缩策略,保留最近几轮加摘要,摘要按事实、决策、未完成项三类抽取。
坑二:多 Agent 互相覆盖状态。现象是两个 Agent 同时改同一份数据,结果错乱。原因是直接共享可变对象。解决是用黑板模式加快照,每个 Agent 读快照、写增量,由协调者合并。
坑三:工具描述模糊导致参数错。现象是模型传的参数类型不对或字段缺失。原因是描述只写了用途没写结构。解决是按 4.1 的格式,把输入参数类型和返回结构写进描述。
坑四:重试策略一刀切导致雪崩。现象是下游限流时,所有 Agent 同时重试,把下游打挂。原因是没区分错误类型。解决是按 4.2 分类,只对超时和限流重试,且加指数退避和随机抖动。
坑五:没有任务 ID 导致问题无法回放。现象是线上出问题,日志散在各处拼不起来。原因是调用日志没串任务 ID。解决是每个任务生成唯一 ID,所有工具调用和模型调用都带上。
5.3 上线前的检查清单
上线前我一般过一遍:工具描述是否都带输入输出结构;记忆压缩是否触发过;重试是否区分错误类型;日志是否带任务 ID;评测集是否覆盖边界和设错任务。这五条过了,基本能挡住大部分线上事故。
6. 进阶技巧:用 Agent 评测反推架构优化
评测不只是验收,更是优化依据。我习惯把评测结果按维度拆开看:如果工具调用准确率低,先查工具描述;如果平均步数高,查规划器是不是拆得太细;如果失败恢复率低,查重试和降级策略。有一次我们发现某类任务步数总是超标,最后定位到是规划器把「查数据」和「分析数据」拆成了两步,其实可以合并,改完步数降了三分之一。
另一个技巧是用评测集做回归。每次改 prompt 或换模型,先跑一遍评测集,看四个指标有没有退化。退化超过 5% 就不上,这是后悔药。评测集不用大,50 到 100 条有代表性的就够,关键是稳定、可重复。
还有一个容易被忽略的点:Agent 的「拒绝能力」。有些任务模型不该接,比如超出权限的查询、明显错误的指令。评测集里要放这类任务,看 Agent 会不会硬做。硬做比做错更危险。
最后说个习惯:我每次调 Agent,先看日志里的步数和耗时分布,再看具体哪步慢、哪步错。不看日志直接改 prompt,基本是玄学调参。希望帮到你。
本文还有配套的精品资源,点击获取