Agent 框架 06:手搓 Agent——从零手写轻量运行底座 Harness

在 AI 智能体开发的世界里,我们经常会遇到这样的困境:为了让大模型调用一两个简单的天气查询或数据库接口,不得不引入庞大的第三方框架。随之而来的是深达数十层的堆栈调用、晦涩难懂的中间件类继承、黑盒一般的内部提示词注入,以及版本升级带来的 API 破裂。

其实,所有第三方 Agent 框架的本质,都是别人封装好的“运行底座”(Harness)。剥离掉所有花哨的概念包装后,一个智能体的底层运行逻辑极其质朴。只有亲手实现过一遍最核心的 Agent 循环,你在评估 LangChain、Semantic Kernel 或 AutoGen 时,才能一眼看穿框架在做什么,并清晰判断何时该用框架,何时自建最可控。


Agent 的底层运行本质:带状态的决策循环

从控制论和图灵机的视角看,任何单智能体系统的核心运行拓扑就是一个带反馈的有限状态循环:

用极简的伪代码描述,仅需几行:

1
2
3
4
5
6
7
while not reached_max_steps:
decision = llm.generate(history + system_prompt, tools=tool_schemas)
if decision.wants_tool:
result = execute_tool(decision.tool_name, decision.tool_args)
history.append(result)
else:
return decision.final_text

所有框架无非是在这个循环周围,加上了重试机制、状态检查点、可观测性埋点和序列化支持。


完整实现:200 行纯 Python 手写 Agent Harness

我们不需要安装任何第三方 Agent 库,仅依赖官方原厂 SDK(此处以 Anthropic Claude API 为例,OpenAI SDK 逻辑完全同构),实现一个生产级的 Agent 运行底座。

1. 结构化工具注册表(ToolRegistry)

首先实现一个通过装饰器注册工具、自动收集 Schema 并统一派发调用的中枢管理器:

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
import inspect
from typing import Any, Callable

class ToolRegistry:
"""轻量级工具注册中枢:负责 Schema 管理、动态分发与安全防护"""

def __init__(self):
self._schemas: dict[str, dict] = {}
self._handlers: dict[str, Callable] = {}

def register(self, name: str, description: str, parameters: dict, func: Callable):
"""显式注册工具"""
self._schemas[name] = {
"name": name,
"description": description,
"input_schema": parameters,
}
self._handlers[name] = func

def tool(self, name: str, description: str, parameters: dict):
"""装饰器注册语法糖"""
def decorator(func: Callable):
self.register(name, description, parameters, func)
return func
return decorator

def get_schemas(self) -> list[dict]:
"""导出所有提供给大模型的工具 JSON Schema 列表"""
return list(self._schemas.values())

def call(self, name: str, args: dict) -> Any:
"""根据名称动态执行目标本地函数,带有完善的异常兜底"""
if name not in self._handlers:
return f"执行失败:未找到名为 '{name}' 的已注册工具"
try:
handler = self._handlers[name]
return handler(**args)
except Exception as e:
return f"工具 '{name}' 运行时抛出异常: {e}"

# 全局工具注册实例
registry = ToolRegistry()

2. 定义实际业务工具

我们定义三个典型的原子工具:气象查询、安全数学运算与轻量知识库检索:

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
@registry.tool(
name="get_weather",
description="查询指定城市的实时气象数据",
parameters={
"type": "object",
"properties": {
"city": {"type": "string", "description": "城市名称,例如北京、上海"},
},
"required": ["city"],
},
)
def get_weather(city: str) -> str:
mock_db = {
"北京": "晴空万里,23°C,北风二级",
"上海": "阴有阵雨,19°C,空气湿度 85%",
"深圳": "多云,27°C,体感舒适",
}
return mock_db.get(city, f"{city} 暂无气象台站数据")


@registry.tool(
name="calculate",
description="高精度数学表达式求值计算器",
parameters={
"type": "object",
"properties": {
"expression": {"type": "string", "description": "标准数学运算表达式,例如 '12 * 8 + 4'"},
},
"required": ["expression"],
},
)
def calculate(expression: str) -> str:
# 防范代码注入:仅允许纯数学字符
allowed_chars = set("0123456789+-*/()., ")
if not all(c in allowed_chars for c in expression):
return "安全校验失败:表达式包含不合法字符"
try:
# 在隔离的作用域内计算
res = eval(expression, {"__builtins__": None}, {})
return str(res)
except Exception as e:
return f"数学运算错误: {e}"


@registry.tool(
name="query_knowledge",
description="在本地系统技术文档库中检索指定名词或规范",
parameters={
"type": "object",
"properties": {
"keyword": {"type": "string", "description": "需要查询的关键词"},
},
"required": ["keyword"],
},
)
def query_knowledge(keyword: str) -> str:
docs = {
"agentscope": "AgentScope 是阿里巴巴达摩院开源的多智能体框架,主打以消息(Msg)为一等公民和 RPC 分布式部署。",
"semantic kernel": "Semantic Kernel 是微软主导的企业级 AI SDK,深度集成 C#/.NET 与依赖注入插件体系。",
"eino": "Eino 是字节跳动开源的 Go 语言 AI 框架,针对生产级高并发与全链路流式设计。",
}
kw_lower = keyword.lower()
for k, v in docs.items():
if k in kw_lower:
return v
return f"文档库中未收录与 '{keyword}' 匹配的知识条目"

3. Agent 运行循环核心容器

接下来编写驱动大模型与工具交互的 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
import os
from anthropic import Anthropic

class AgentHarness:
"""轻量级 Agent 运行底座"""

def __init__(
self,
system_prompt: str,
model: str = "claude-opus-4-5",
max_iterations: int = 8,
verbose: bool = True,
):
self.client = Anthropic()
self.system_prompt = system_prompt
self.model = model
self.max_iterations = max_iterations
self.verbose = verbose
self.history: list[dict] = []

def _log(self, text: str):
if self.verbose:
print(text)

def run(self, user_prompt: str) -> str:
"""执行单次任务直至生成最终结论或触发步数熔断"""
# 将用户目标追加至历史对话列表
self.history.append({"role": "user", "content": user_prompt})

for step in range(1, self.max_iterations + 1):
self._log(f"\n[🔄 步数 {step}/{self.max_iterations}] 正在等待大模型规划决策...")

response = self.client.messages.create(
model=self.model,
max_tokens=2048,
system=self.system_prompt,
tools=registry.get_schemas(),
messages=self.history,
)

# 分支 1:模型判断需要调用外部工具
if response.stop_reason == "tool_use":
tool_results = []
for block in response.content:
if block.type == "tool_use":
self._log(f" 👉 [调用工具] {block.name}(参数: {block.input})")
exec_res = registry.call(block.name, block.input)
self._log(f" 👈 [工具输出] {exec_res}")

tool_results.append({
"type": "tool_result",
"tool_use_id": block.id,
"content": str(exec_res),
})

# 将模型的工具调用意图与执行结果作为多轮历史写回
self.history.append({"role": "assistant", "content": response.content})
self.history.append({"role": "user", "content": tool_results})

# 分支 2:模型认为已获得足够信息,输出最终文本回答
elif response.stop_reason == "end_turn":
final_answer = ""
for block in response.content:
if hasattr(block, "text"):
final_answer += block.text

self.history.append({"role": "assistant", "content": final_answer})
self._log(" ✅ [任务完成] 模型已生成最终答复。")
return final_answer

# 防御兜底:防止提示词死循环导致无限消耗 API Token
warning = "⚠️ [熔断触发] 已达到最大迭代步数限制,当前任务被强制中断。"
self._log(warning)
return warning

def reset(self):
"""清空会话历史"""
self.history = []

运行与实测验证

我们初始化 Agent 并测试跨多个工具的复合多步推理能力:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
if __name__ == "__main__":
harness = AgentHarness(
system_prompt="""你是一名严谨的智能技术助手。
你可以自主调用天气查询、数学计算与技术知识库检索工具来协助回答问题。
必须基于工具返回的事实给出答案,严禁凭空捏造。""",
verbose=True,
)

query = (
"帮我查一下北京现在的天气;"
"另外算一下 128 乘以 3.5 加上 42 等于多少;"
"最后在文档库里检索一下 AgentScope 是什么。"
)

print("=" * 60)
print("用户指令:", query)
print("=" * 60)

final_result = harness.run(query)

print("\n" + "=" * 60)
print("智能体最终输出:\n", final_result)
print("=" * 60)

在运行日志中,你可以直观地观察到:

  1. 第 1 步:模型识别出 3 个相互独立的目标,单次响应中并发输出了 3 个 tool_use 请求(get_weather、calculate、query_knowledge)。
  2. 工具执行:ToolRegistry 依次安全执行每个本地函数,构造标准化 tool_result 切片回传。
  3. 第 2 步:模型读取到包含 3 个结果的新上下文,进入 end_turn 分支,合成结构化的完整中文答复。

进阶强化:多工具并发加速

当大模型在一次决策中请求多个独立的 I/O 工具(例如并发抓取多个网页或查询不同城市天气)时,单线程顺序执行会徒增首字等待时间。我们可以利用 Python concurrent.futures 轻松实现并发提速:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
import concurrent.futures

def execute_tools_parallel(tool_calls: list) -> list[dict]:
"""多线程并发执行工具列表"""
results = []
with concurrent.futures.ThreadPoolExecutor(max_workers=5) as executor:
future_map = {
executor.submit(registry.call, tc.name, tc.input): tc
for tc in tool_calls
}
for future in concurrent.futures.as_completed(future_map):
tc = future_map[future]
try:
res = future.result()
except Exception as e:
res = f"线程池执行失败: {e}"
results.append({
"type": "tool_result",
"tool_use_id": tc.id,
"content": str(res),
})
return results

框架与手写 Harness 的概念映射表

通过手写这 200 行代码,再去看成熟框架,你会发现它们的本质是一一对应的:

智能体运行时要素 手写 Harness 的对应代码 常见商业 / 开源框架中的包装
工具定义与元数据 ToolRegistry.register / @registry.tool LangChain @tool / Semantic Kernel KernelFunction
核心执行拓扑 for step in range(max_iterations) 循环 LangChain AgentExecutor / Mastra Workflow.step
工具派发路由 registry.call(name, args) LangGraph ToolNode / Eino ToolComponent
上下文状态维护 self.history.append(...) LangGraph StateGraph(messages) / AutoGen ChatResult
防死循环熔断保护 max_iterations = 8 判定退出 框架内部隐藏的 max_turns 参数

决策准则:何时手搓 Harness,何时选用框架?

推荐手搓自研 Harness 的场景

  1. 核心逻辑清晰单一:业务只需挂载 3~5 个确定的内部微服务 API,无需复杂的学术图算法。
  2. 生产环境对黑盒零容忍:金融、医疗或交易核心链路要求每一行代码可断点调试、日志可追溯、无第三方未审计依赖。
  3. 性能敏感与依赖极简:希望避免安装臃肿的几百兆三方库,减少部署包体积与安全漏洞扫描风险。

推荐选用成熟框架的场景

  1. 多智能体复杂群聊/辩论:需要编排像 AutoGen、AgentScope 那样涉及十几个 Agent 的动态协作与路由。
  2. 长生命周期人机协同:需要 LangGraph 级别的会话检查点持久化(Checkpoint)、时间旅行回溯与人工审批阻断。
  3. 团队统一全栈基建:如前端 TypeScript 团队直接使用 Mastra / Vercel AI SDK,与 Next.js 生态无缝对接。

关联导航