agent动手实践

1. 极简 Agent

很多人以为 Agent 底层有某种魔法,其实本质上就是一个 while 驱动的状态循环:

1
2
3
4
5
6
7
8
9
10
11
用户提问 

[LLM 决策] → 是否需要工具?
├── 否(直接回答) → 输出并结束循环
└── 是(返回函数名与参数)

[本地 Python 执行函数]

[将执行结果塞回上下文 messages]

[回到 LLM 决策,继续下一轮思考]

1.1 代码

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
"""最小 ReAct Agent:模型可以调用本地计算器,而不是自己心算。

判断某一轮走了哪条路,看这两个信号:
- finish_reason == "tool_calls" 且 msg.tool_calls 有值 -> 跑了本地工具
- finish_reason == "stop" 且 msg.tool_calls 为空 -> 给出最终自然语言回答
"""

import json
import os
from pathlib import Path

from openai import OpenAI

# OpenCode Go 兼容 OpenAI 接口,改 base_url 就能用官方 SDK。
OPENCODE_GO_BASE_URL = "https://opencode.ai/zen/go/v1"
MODEL = os.environ.get("OPENCODE_MODEL", "glm-5.1")


def load_api_key() -> str:
"""优先读环境变量,没有再用本机 OpenCode 登录态里的 key。"""
key = os.environ.get("OPENCODE_API_KEY") or os.environ.get("OPENCODE_GO_API_KEY")
if key:
return key
auth_path = Path.home() / ".local/share/opencode/auth.json"
if auth_path.exists():
data = json.loads(auth_path.read_text())
for provider in ("opencode-go", "opencode"):
stored = data.get(provider) or {}
if stored.get("key"):
return stored["key"]

raise RuntimeError(
"Missing OpenCode API key. Set OPENCODE_API_KEY, or log in with `opencode`."
)


client = OpenAI(base_url=OPENCODE_GO_BASE_URL, api_key=load_api_key())


# --- 1. 本地工具:模型只能“点名”,真正执行发生在这里 ---

def calculate(expression: str) -> str:
try:
return str(eval(expression))
except Exception as e:
return f"Error: {e}"


# 模型返回的是函数名字符串,用这张表在本地找到对应的 Python 函数。
tool_map = {"calculate": calculate}


# --- 2. 发给模型的工具 Schema(它看不到上面的 Python 函数) ---

tools = [{
"type": "function",
"function": {
"name": "calculate",
"description": "Evaluate a math expression",
"parameters": {
"type": "object",
"properties": {
"expression": {
"type": "string",
"description": "For example: 123 * 456",
}
},
"required": ["expression"],
},
},
}]


# --- 3. ReAct 循环:模型思考 -> 可能调工具 -> 把结果喂回去 ---

messages = [{"role": "user", "content": "帮我算一下 248 乘以 15 等于多少?"}]

round_num = 0
while True:
round_num += 1
response = client.chat.completions.create(
model=MODEL,
messages=messages,
tools=tools,
)
choice = response.choices[0]
msg = choice.message
print(f"\n--- round {round_num} ---")
print("finish_reason:", choice.finish_reason)

# 把这一轮 assistant 消息写入历史;后面要把 tool_call id 对上。
assistant_msg = {"role": "assistant", "content": msg.content or ""}
if msg.tool_calls:
assistant_msg["tool_calls"] = [
{
"id": tool_call.id,
"type": "function",
"function": {
"name": tool_call.function.name,
"arguments": tool_call.function.arguments,
},
}
for tool_call in msg.tool_calls
]
messages.append(assistant_msg)

# 没有 tool_calls 说明模型已经直接给最终答案,结束循环。
if not msg.tool_calls:
print("path: model returned the answer directly (no tool_calls)")
print("Agent final answer:", msg.content)
break

print("path: model requested tool_calls")
for tool_call in msg.tool_calls:
func_name = tool_call.function.name
func_args = json.loads(tool_call.function.arguments)

# 本地执行工具,再把结果追加进 messages,下一轮模型才能看到。
result = tool_map[func_name](**func_args)
print(f" call {func_name}({func_args}) -> {result}")
messages.append({
"role": "tool",
"tool_call_id": tool_call.id,
"content": result,
})

  • python a.py
1
2
3
4
5
6
7
8
9
--- round 1 ---
finish_reason: tool_calls
path: model requested tool_calls
call calculate({'expression': '248 * 15'}) -> 3720

--- round 2 ---
finish_reason: stop
path: model returned the answer directly (no tool_calls)
Agent final answer: 248 乘以 15 等于 3720。

1.2 Agent 执行流程

大模型只负责 “ 动脑决策 “,你的本地 Python 代码才负责 “ 动手算数/执行算法 “!

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
[1. 用户提问] ──> "248 乘以 15 是多少?"


[2. 大模型(云端大脑)]
• 思考:我心算不靠谱,但我看到开发者给了我一个 calculate 工具!
• 输出决策:"请帮我运行 calculate(expression='248 * 15')" (此时它还没做任何计算)


[3. 本地 Python 代码(你的电脑/服务器)]
• 抓取到大模型的工具调用指令
• 实际执行算法:在本地 CPU 上运行真正的 Python 代码 `248 * 15`,得出 `3720`


[4. 二次送给大模型(汇报结果)]
• 你的代码把结果打包追加到 messages 中:{"role": "tool", "content": "3720"}
• 再次调用 API,大模型看到真实计算结果后组织语言:"248 乘以 15 的结果是 3720。"

1.3 提问问题

操作 A:向数据库发起 SELECT * FROM users; 查询用户列表。

操作 B:分析用户的问题,并从 5 个可用工具中挑选出应该调用 “ 数据库查询工具 “ 而不是 “ 天气工具 “。

哪一个是大模型(LLM)完成的?哪一个是你的本地 Python 代码完成的?

  • 操作 B(分析意图并选工具)由大模型(LLM)完成:这需要对人类自然语言进行语义理解与逻辑推理,是 “ 大脑 “ 的专长。
  • 操作 A(执行 SQL 语句查询数据库)由本地 Python 代码完成:大模型没有真实的网络套接字或数据库连接权限,它只负责输出 SQL 语句或参数,真正向数据库发起网络 I/O、获取数据表结果并返回的,是运行在服务器上的本地 Python 代码。

2. 反思机制(Reflection)与短期记忆(Memory) TODO

Agent 虽然能调用工具,但有两个明显的弱点:

  • 记不住历史(没有记忆):每次对话都是全新的 messages,聊完就忘。
  • 缺乏质检(没有反思):本地工具返回什么,它就信什么;一旦代码写错或工具给出的数据格式异常,它不会主动审查结果。

3. 参考资料

如果这篇文章对你有帮助,不介意的话,请作者喝杯咖啡吧
levon 微信 微信
levon 支付宝 支付宝