-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathstep3_react_loop.py
More file actions
142 lines (114 loc) · 5.36 KB
/
Copy pathstep3_react_loop.py
File metadata and controls
142 lines (114 loc) · 5.36 KB
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
137
138
139
140
141
142
"""
第 3 步:把手工过程包成循环 —— 这就是 ReAct。
运行:
python tutorial/step3_react_loop.py
step2 里我们手动跑完了「问 → 要工具 → 执行 → 回填 → 再问」。
这一步不引入任何新概念,只做一件事:**把它包进 while 循环**。
Reason (推理) 模型看着当前对话,决定下一步做什么
Act (行动) 如果它要工具,我们就执行
Observe (观察) 把结果写回对话,让模型「看见」
这三拍转起来,就是 ReAct(Reasoning + Acting)。
Agent 之所以能自己完成多步任务,全部秘密就在这个循环里:
每转一圈,对话就长一点,模型掌握的信息就多一点,直到它说「够了,答案是……」。
两个必须有的护栏:
- max_steps:模型可能反复调同一个工具停不下来,必须有硬性上限
- 循环出口:模型返回的消息里没有 tool_calls,就代表它认为任务完成了
"""
import ast
import json
import operator
import pathlib
import sys
sys.path.insert(0, str(pathlib.Path(__file__).resolve().parent.parent))
import config # noqa: E402
CALCULATOR_SCHEMA = {
"type": "function",
"function": {
"name": "calculator",
"description": "计算一个数学表达式,返回精确结果。需要算数时必须用它,不要心算。",
"parameters": {
"type": "object",
"properties": {
"expression": {"type": "string", "description": "如 '(12+8)*3'"}
},
"required": ["expression"],
},
},
}
def calculator(expression: str) -> str:
"""和 step2 一样的安全计算器(AST 白名单,不用 eval)。"""
operators = {
ast.Add: operator.add,
ast.Sub: operator.sub,
ast.Mult: operator.mul,
ast.Div: operator.truediv,
ast.USub: operator.neg,
}
def walk(node):
if isinstance(node, ast.Expression):
return walk(node.body)
if isinstance(node, ast.Constant) and isinstance(node.value, (int, float)):
return node.value
if isinstance(node, ast.BinOp) and type(node.op) in operators:
return operators[type(node.op)](walk(node.left), walk(node.right))
if isinstance(node, ast.UnaryOp) and type(node.op) in operators:
return operators[type(node.op)](walk(node.operand))
raise ValueError(f"不支持的表达式:{expression!r}")
return str(walk(ast.parse(expression, mode="eval")))
# 工具名 → 函数。有了它,执行阶段就不用写一长串 if/elif。
# 这个 dict 就是 tools/registry.py 的雏形。
HANDLERS = {"calculator": calculator}
def react(goal: str, max_steps: int = 5) -> str:
"""ReAct 主循环。这 30 行就是 agent/loop.py 的骨架。"""
client = config.get_client()
messages = [
{"role": "system", "content": "你是一个助手,需要算数时必须使用 calculator 工具。"},
{"role": "user", "content": goal},
]
for step in range(1, max_steps + 1):
print(f"\n--- 第 {step} 圈 ---")
# ---- Reason:把当前完整对话发给模型 ----
# 注意每一圈发的都是**全部** messages,不是增量。
# 模型本身没有记忆,它的「记忆」就是我们每次重新塞给它的这个列表。
response = client.chat.completions.create(
model=config.MODEL,
messages=messages,
tools=[CALCULATOR_SCHEMA],
)
message = response.choices[0].message
# 不管有没有工具调用,都先把模型这条消息记进对话。
# 漏了这步,后面的 tool 消息就会找不到它要配对的 assistant,API 直接报错。
messages.append({
"role": "assistant",
"content": message.content or "",
"tool_calls": [tc.model_dump() for tc in message.tool_calls or []],
})
# ---- 出口:没有 tool_calls,说明模型认为任务完成了 ----
if not message.tool_calls:
print(f"[Answer] {message.content}")
return message.content
# ---- Act + Observe ----
for tool_call in message.tool_calls:
name = tool_call.function.name
arguments = json.loads(tool_call.function.arguments)
print(f"[Act] {name}({tool_call.function.arguments})")
# 工具出错不要往上抛:把错误信息也当作一种「观察」还给模型,
# 它往往能自己换个参数重试。这比直接崩掉有用得多。
try:
result = HANDLERS[name](**arguments)
except Exception as exc: # noqa: BLE001 — 教学演示,故意兜住所有异常
result = f"[工具出错] {type(exc).__name__}: {exc}"
print(f"[Observe] {result}")
messages.append({
"role": "tool",
"tool_call_id": tool_call.id,
"content": result,
})
return f"[已达最大步数 {max_steps},任务未完成]"
if __name__ == "__main__":
print("=" * 60)
answer = react("帮我算一下 (12+8)*3 等于多少?")
print("=" * 60)
print()
print("对照着看 agent/loop.py —— 结构完全一样,只是把 messages 抽成了 Memory 类。")
print("下一步(step4):让 Agent 记住上一轮说过什么。")