AI Agent 落地工程化与选型实践:从 PoC 到生产的 10 个关键决策

AI Agent 落地工程化与选型实践:从 PoC 到生产的 10 个关键决策

技术主题:AI Agent(人工智能代理)
内容方向:实际使用经验分享(工具/框架选型与工程化落地心得)

引言

很多团队的 Agent 项目能在 PoC 阶段跑出“惊艳的 demo”,但一到生产就暴露出时延失控、幻觉严重、工具易坏、难以回溯等问题。本文结合一次企业知识助手 + 操作执行(工单/日历/审批)的落地经验,提炼 10 个关键决策点,并给出一份最小可用的 Python 骨架,覆盖工具规范、超时重试、预算控制、追踪与灰度发布,帮助你把 Agent 从“能跑”推到“可控、可回归、可演进”。

一、10 个关键决策(经验总结)

  1. 工具规范先行(Tool-First)
  • 以 OpenAPI/JSON Schema 定义工具参数与返回,禁止非结构化模糊描述;
  • 工具文档与示例对 LLM 可见,但敏感字段通过别名与服务端注入。
  1. 单一职责的工具粒度
  • 不要做“大而全”的 mega-tool;分解为可组合的原子动作,便于重试与幂等。
  1. 强约束的调用协议
  • 统一“计划-执行-校验”模板;生成失败/参数不全时快速回退;
  • 引入 schema 验证与默认值填补,避免 LLM 产出非法 payload。
  1. 超时与重试的分层策略
  • 工具层:connect/read/total 超时 + 指数抖动退避 + 上限;
  • 编排层:总预算时间与 token 上限,必要时降级或短路。
  1. 记忆与检索的边界
  • 短期对话上下文、长期事实(profile/偏好)、外部知识(RAG)明确边界;
  • 检索与工具调用一样是“第一类工具”,由 Planner 显式触发。
  1. 可观测性与可回溯
  • 每次调用记录:prompt、工具参数、耗时、结果摘要、引用证据;
  • 失败原因分类,留存最小可复现实验包(prompt+tools+seeds)。
  1. 评测集与灰度发布
  • 构建稳定的离线评测集(问题-期望证据/答案),上线前 A/B;
  • 线上灰度:10% 流量影子执行,对比引用率、成本与满意度。
  1. 幂等、防抖与去重
  • 工具业务侧基于幂等键;流水线侧去重队列,防止网络抖动触发重复动作。
  1. 敏感操作的授权
  • 高风险工具必须显式二次确认或用户态签名,Agent 只出“建议变更计划”。
  1. 成本与时延治理
  • 统一模型与上下文策略,优先短上下文模型;
  • 热路径缓存(工具结果、解析器)、批量化工具调用,控制 P95。

二、最小可用骨架(Python)

下面的代码演示:

  • ToolSpec/ToolRunner:超时重试/结构化参数验证;
  • Budget:编排层预算控制;
  • Tracer:关键链路追踪;
  • Planner/Agent:计划-执行-校验-降级。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
from dataclasses import dataclass, field
from typing import Any, Dict, Callable, Optional, List
import time, json, random
import asyncio

# -------- 工具规范与运行器 --------
@dataclass
class ToolSpec:
name: str
schema: Dict[str, Any] # 简化:JSON Schema 子集
runner: Callable[[Dict[str, Any]], Any]
timeout_s: float = 8.0
retry: int = 2

class SchemaError(ValueError):
pass

def validate(schema: Dict[str, Any], payload: Dict[str, Any]) -> Dict[str, Any]:
required = schema.get("required", [])
props = schema.get("properties", {})
for k in required:
if k not in payload:
raise SchemaError(f"missing field: {k}")
# 简单默认值填补
for k, v in props.items():
if k not in payload and "default" in v:
payload[k] = v["default"]
return payload

async def run_with_retry(tool: ToolSpec, payload: Dict[str, Any]) -> Any:
payload = validate(tool.schema, dict(payload))
delay = 0.5
for attempt in range(tool.retry + 1):
try:
return await asyncio.wait_for(asyncio.to_thread(tool.runner, payload), timeout=tool.timeout_s)
except Exception as e:
if attempt >= tool.retry:
raise
await asyncio.sleep(delay + random.random()*0.2)
delay = min(delay * 2, 3.0)

# -------- 预算与追踪 --------
@dataclass
class Budget:
tokens: int
ms: int
start: float = field(default_factory=lambda: time.time())
def left_ms(self) -> int:
return int(self.ms - (time.time() - self.start) * 1000)

@dataclass
class TraceEvent:
name: str
at: float
meta: Dict[str, Any]

class Tracer:
def __init__(self):
self.events: List[TraceEvent] = []
def log(self, name: str, **meta):
self.events.append(TraceEvent(name, time.time(), meta))
def dump(self) -> List[Dict[str, Any]]:
return [dict(name=e.name, at=e.at, meta=e.meta) for e in self.events]

# -------- Planner 与 Agent --------
class Planner:
def decide(self, query: str) -> Dict[str, Any]:
need_search = any(k in query for k in ["规范","流程","价格","说明"])
tools = ["search"] if need_search else []
return {"tools": tools, "k_refs": 2}

class DummyLLM:
def generate(self, prompt: str, max_tokens: int = 500) -> str:
# 真实项目替换为 LLM 客户端;此处生成一个示例说明
return "答复:请参考[1][2],并已创建日程。"

class Agent:
def __init__(self, tools: Dict[str, ToolSpec], llm: DummyLLM):
self.tools = tools
self.llm = llm
self.planner = Planner()
self.tracer = Tracer()

async def run(self, query: str) -> Dict[str, Any]:
plan = self.planner.decide(query)
budget = Budget(tokens=3000, ms=3000)
self.tracer.log("plan", plan=plan)

evidences = []
for t in plan["tools"]:
if budget.left_ms() < 400: break
self.tracer.log("tool.call", name=t)
res = await run_with_retry(self.tools[t], {"q": query, "k": 4})
evidences.extend(res)
self.tracer.log("tool.ok", name=t, size=len(res))

# 生成 + 引用约束(示例)
ctx = "\n".join([f"[{i+1}] {e['title']}" for i, e in enumerate(evidences[:6])])
prompt = f"基于证据回答并在结尾引用:[示例]\n{ctx}\n问题:{query}"
ans = self.llm.generate(prompt)
used = [int(x) for x in __import__('re').findall(r"\[(\d+)\]", ans)]
if len(used) < plan["k_refs"]:
# 降级:缩小上下文重试一次(示意)
ans = self.llm.generate(prompt[:600])
return {"answer": ans, "trace": self.tracer.dump()}

# -------- 工具示例:search(替代为你的 RAG 检索)--------
def search_runner(payload: Dict[str, Any]):
q = payload["q"]
k = payload.get("k", 4)
# 伪实现:返回 k 条“证据”
return [{"title": f"{q}-证据-{i+1}", "url": f"https://kb/{i+1}"} for i in range(k)]

search_tool = ToolSpec(
name="search",
schema={
"type": "object",
"properties": {
"q": {"type": "string"},
"k": {"type": "integer", "default": 4}
},
"required": ["q"]
},
runner=search_runner,
timeout_s=2.5,
retry=1
)

# 运行示例(脚本入口)
async def demo():
agent = Agent(tools={"search": search_tool}, llm=DummyLLM())
out = await agent.run("发布流程规范与审批")
print(json.dumps(out, ensure_ascii=False, indent=2))

if __name__ == "__main__":
asyncio.run(demo())

要点:

  • 工具是“第一类公民”,先验 schema,再在受控的超时/重试下执行;
  • 编排层有时间预算与降级路径,避免请求“跑飞”;
  • 追踪保留 plan、工具调用与结果摘要,方便复现实验。

三、评测与灰度:怎么把“感觉不错”变成“可量化好”

  • 离线评测集:100-300 条业务问题,标注应命中文档与片段,评估召回@K、引用命中率、答案一致性;
  • 线上灰度:影子执行对比引用数量、P95 时延、平均 token 成本、满意度打分;
  • 回归门禁:每次 Prompt/模型/工具更新必须通过评测阈值才允许发布。

四、常见坑与对策

  • 一把梭 LLM:把“搜索/工具/记忆”揉进一个大 Prompt,生产必崩 → 分层与工具化;
  • 无约束调用:工具异常不报错、参数缺失照样执行 → schema 校验 + 失败快返;
  • 重试失控:无上限重试导致爆表 → 指数退避 + 上限 + 幂等;
  • 观测缺位:线下复现困难 → 全链路追踪 + 最小复现场景打包;
  • 无灰度:直接全量 → 影子执行 + 百分比灰度 + 快速回滚策略。

总结

从 PoC 到生产,Agent 项目的成败不在“模型多聪明”,而在“工程化是否扎实可控”。把工具当第一类公民,用 schema 与超时重试兜住下限;在编排层实施预算与降级;建设评测与观测闭环,并坚持灰度发布。文中的 Python 骨架可以作为最小起点,你可以替换 RAG、LLM 与工具实现,快速搭建一条“能跑且可控”的 Agent 生产链路。