热闻岛
返回全网热点

为什么有时候要“手搓”Agent?

1小时前8 阅读
为什么有时候要“手搓”Agent?配图
核心观点:框架适合起步,手写适合掌控。真正成熟的工程方案,不是“框架党”和“手搓党”二选一,而是把 Agent 的核心决策链路掌握在自己手里,把非核心能力交给成熟生态。 01 手搓不是炫技,是为了拿回控制权 做 Agent 项目时,很多人第
为什么有时候要“手搓”Agent?配图

核心观点:框架适合起步,手写适合掌控。真正成熟的工程方案,不是“框架党”和“手搓党”二选一,而是把 Agent 的核心决策链路掌握在自己手里,把非核心能力交给成熟生态。

01 手搓不是炫技,是为了拿回控制权

做 Agent 项目时,很多人第一反应是直接上 LangChain、LangGraph、OpenAI Agents SDK、CrewAI 这类框架。这样做没错,尤其在 POC 阶段,框架能帮你快速跑通工具注册、模型调用、消息维护和 ReAct 循环。

但项目一旦进入生产环境,问题会变得非常具体:某一次工具参数为什么错了?消息顺序为什么乱了?某个工具失败后为什么没有重试?一次调用到底花了多少 Token?线上事故能不能完整回放?

这些问题,靠“框架帮我封装好了”是不够的。越核心的链路,越需要可观测、可解释、可回放、可裁剪。所谓手搓 Agent,本质不是排斥框架,而是把决定系统行为的核心逻辑写清楚。

为什么有时候要“手搓”Agent?配图

02 框架为什么一开始很好用?

从零写一个 Agent,并不只是调用一次大模型。至少要处理这几件事:

定义工具 Schema,让模型知道工具叫什么、能做什么、参数怎么传。

解析模型返回的 tool_calls,判断是否要继续执行工具。

维护 messages 顺序,把 assistant 消息和 tool 结果正确写回上下文。

处理工具失败、参数缺失、超时、重试、权限校验。

保存任务状态、运行日志、Token 成本、每轮输入输出,方便回放。

框架的价值在这里:把大量重复样板封装掉。比如 LangChain 可以用较少代码搭起模型、工具和 Agent 循环;OpenAI Agents SDK 则把 Agent、Tools、Handoffs、Guardrails、Tracing 等概念整理成一套更完整的运行时。

所以,不要一开口就否定框架。框架在探索期是真好用,问题是:它适不适合把核心生产链路长期交出去。

03 痛点什么时候开始出现?

框架的问题通常不会在第一天出现。它常常是随着项目阶段变化,一层一层浮出来。

为什么有时候要“手搓”Agent?配图

3.1 第一个痛点:调试链路太深

代码看起来只有几十行,但真正执行时,内部可能穿过 Prompt 模板、Runnable、Parser、Callback、ToolNode、Memory 等多层抽象。线上出现错误时,你看到的是框架栈,而不是业务链路。

真正难的不是“报错”,而是你不知道问题到底发生在哪一层:是模型输出错了,还是参数解析错了?是工具本身失败,还是框架在某个 callback 阶段改了状态?

3.2 第二个痛点:版本升级带来不确定性

框架生态变化快,这是 AI 工程的现实。接口迁移、默认行为变化、旧 API 逐步废弃,都可能影响线上稳定性。核心业务逻辑越依赖框架内部语义,升级成本越不可控。

3.3 第三个痛点:通用设计带来隐性开销

框架为了兼容更多场景,会做很多通用处理:序列化中间结果、触发 callback、维护复杂状态、记录大量上下文。低流量时不明显,高并发时就会变成真实延迟和成本。

04 手搓 Agent 的核心:不是少写代码,而是看得见每一步

手写 Agent 的最小闭环其实并不复杂:模型看消息,决定是否调用工具;应用层执行工具,把结果写回消息;模型再看新的上下文,直到输出最终答案或达到终止条件。

为什么有时候要“手搓”Agent?配图

这件事一旦自己写,系统行为会变得非常清楚:每一轮模型看到了什么、为什么调用这个工具、工具参数是什么、结果如何写回、什么时候停止,都能被记录和复现。

05 Tool Calling 的本质:模型只负责“提出调用”,应用负责“真正执行”

很多人第一次接触 Tool Calling 时,会误以为模型真的在执行函数。实际上,模型只是在结构化输出里表达“我想调用某个工具,并给出参数”。真正的函数执行发生在你的应用代码里。

为什么有时候要“手搓”Agent?配图

这也是手写 Agent 最该掌控的部分。因为这里涉及权限、数据安全、参数校验、失败重试和审计日志,不能完全依赖模型自己“自觉”。

06 一个最小可用的手写 Agent 架构

生产级手写 Agent 不等于一个裸 for 循环。for 循环只是心脏,周围还需要 LLM 客户端、工具注册表、状态管理、策略护栏和观测系统。

为什么有时候要“手搓”Agent?配图

07 代码骨架:把 Agent Loop 写在自己手里

下面的代码不是完整 SDK,而是展示核心思想:工具注册、模型调用、工具执行、结果回写、终止条件,都放在应用层可控的位置。

7.1 工具 Schema:先让模型知道能调用什么

tool_schemas = [
{
"type": "function",
"function": {
"name": "search_order",
"description": "根据订单号查询订单状态",
"parameters": {
"type": "object",
"properties": {
"order_id": {
"type": "string",
"description": "订单号,例如 A1024"
}
},
"required": ["order_id"]
}
}
}
]

7.2 工具注册表:不要让模型随便调函数

def search_order(order_id: str) -> dict:
# 真实场景里可以访问数据库、HTTP API 或内部 RPC
return {"order_id": order_id, "status": "已发货", "eta": "明天 18:00 前"}

TOOL_REGISTRY = {
"search_order": search_order,
}

def execute_tool(name: str, arguments: dict):
if name not in TOOL_REGISTRY:
raise ValueError(f"工具不在白名单中: {name}")

return TOOL_REGISTRY[name](**arguments)

7.3 核心 Loop:模型、工具、消息状态都在明面上

import json
import time

MAX_TURNS = 6

def run_agent(user_input: str):
messages = [
{"role": "system", "content": "你是一个谨慎的订单助手。"},
{"role": "user", "content": user_input},
]

for turn in range(MAX_TURNS):
started = time.time()

response = client.chat.completions.create(
model="gpt-4o-mini",
messages=messages,
tools=tool_schemas,
)

msg = response.choices[0].message
messages.append(msg)

log_event(
event="llm_response",
turn=turn,
content=msg.content,
tool_calls=msg.tool_calls,
latency_ms=int((time.time() - started) * 1000),
)

# 没有工具调用,说明模型认为任务可以结束
if not msg.tool_calls:
return msg.content

for tc in msg.tool_calls:
name = tc.function.name
args = json.loads(tc.function.arguments)

try:
result = execute_tool(name, args)
status = "ok"
except Exception as e:
result = {"error": str(e)}
status = "failed"

log_event(
event="tool_result",
turn=turn,
tool=name,
args=args,
status=status,
result=result,
)

messages.append({
"role": "tool",
"tool_call_id": tc.id,
"content": json.dumps(result, ensure_ascii=False),
})

return "任务没有在限定轮次内完成,请补充信息或转人工处理。"

上面这段代码看起来比框架版本长,但它有一个巨大优势:你知道每一个关键动作在哪里发生。以后要加限流、权限、缓存、灰度、人工审批、Tracing,都有明确插入点。

08 生产环境:真正要写清楚的是失败路径

只写正常流程不叫工程。Agent 上线后,大量问题都发生在失败路径上:工具超时、参数不合法、模型循环调用同一个工具、敏感操作没审批、工具返回脏数据、用户输入不完整。

为什么有时候要“手搓”Agent?配图

一个稳定的手写 Agent,至少要有这些控制点:

最大轮次:防止模型无限循环。

工具白名单:模型只能调用允许的工具。

参数校验:缺字段、类型错、越权参数必须拦住。

超时控制:外部 API 不可无限等待。

失败重试:只对可恢复错误重试,并限制次数。

敏感操作审批:转账、删除、发消息、下单等动作必须加人审或二次确认。

结果回放:出现事故时,可以完整复现每一轮输入输出。

09 可观测性:手写 Agent 最大的工程收益

如果一个 Agent 只能看到最终答案,那就像一个没有行车记录仪的自动驾驶系统。生产环境里,你必须记录每一轮决策。

为什么有时候要“手搓”Agent?配图

建议至少记录这些字段:

trace_id:一次任务的唯一 ID。
turn:第几轮循环。
input_messages_hash:输入上下文摘要,避免日志过大。
model:使用的模型版本。
tool_name / tool_args / tool_result:工具调用详情。
latency_ms / token_usage / cost:性能和成本。
decision:这一轮为什么继续、结束、追问或转人工。
error_stack:失败时的异常堆栈。

10 框架、手搓、混合方案怎么选?

选择标准不是“哪个更高级”,而是看项目处在哪个阶段、业务复杂度如何、对控制权要求有多高。

为什么有时候要“手搓”Agent?配图

10.1 适合用框架的场景

POC 阶段,只想快速验证 idea。

团队刚接触 Agent,需要先理解基本模式。

项目主要依赖框架生态,比如文档加载、向量检索、现成工具集。

核心链路不复杂,出错影响较小。

10.2 适合手写核心的场景

已经准备上生产,稳定性和可观测性优先。

业务高度定制,框架默认流程经常绕不过去。

调用量较大,需要精细优化延迟和 Token 成本。

涉及权限、资金、订单、内部系统等敏感操作。

团队必须能复盘每一次 Agent 决策。

10.3 最推荐的方案:核心手写,周边复用

这通常是最务实的选择。Agent Loop、消息状态、工具执行、错误处理、权限审批这些“心脏”自己写;文档解析、向量库客户端、Tracing 平台、评测平台、模型 SDK 这些“零件”可以复用成熟生态。

为什么有时候要“手搓”Agent?配图

11 从框架迁移到手写核心:不要一刀切

真实团队很少一开始就完全手写。更稳的路线是先用框架跑通,再把最需要控制的部分逐步抽出来。

为什么有时候要“手搓”Agent?配图

先用框架快速验证:确认业务价值,不要过早设计。

补足观测:无论用不用框架,都先把日志和 Trace 补齐。

抽出核心 Loop:工具调用、消息维护、终止条件先手写。

加生产护栏:重试、超时、审批、降级、回放。

保留周边生态:文档解析、向量检索、评测、监控继续复用。

12 选型判断树:别问“要不要手搓”,先问“要控制什么”

为什么有时候要“手搓”Agent?配图

如果你能清楚说出框架内部每一步做了什么,并且它的默认行为符合你的业务,那用框架没有问题;如果你只能调用一个方法,但出了问题完全不知道它内部如何决策,那这段就不适合继续黑盒依赖。

13 面试总结

回答“为什么有时候要手搓 Agent”时,不要简单说“框架不好”。更专业的说法应该是:

框架在 POC 阶段价值很大,能快速跑通模型、工具、记忆、回调和 Agent Loop。

但生产阶段最怕核心链路黑盒,调试、性能、稳定性和版本升级都会变成真实成本。

手写 Agent 的价值是完全掌控:消息、工具、状态、异常、日志、终止条件都在自己手里。

最成熟的工程方案不是全手写,而是“核心手写,周边复用”。

最终目标不是少依赖,而是让关键路径可解释、可观测、可回放、可维护。

记住一句话:框架不是问题,不理解就依赖才是问题。手搓也不是目的,掌控关键链路才是目的。

要点速读

核心观点:框架适合起步,手写适合掌控。真正成熟的工程方案,不是“框架党”和“手搓党”二选一,而是把 Agent 的核心决

  • 核心观点:框架适合起步,手写适合掌控
  • 真正成熟的工程方案,不是“框架党”和“手搓党”二选一,而是把 Agent 的核心决
  • 更多细节仍在持续更新中