复刻 Cursor 的 ReAct 循环写 200 行 Agent,只用 TaoToken 发的 Key 行不行?
2026/9/21 0:37:29 网站建设 项目流程

1. 复刻 Cursor 的 ReAct 循环:200 行 agent.py 到底把 Key 放哪层

复刻 Cursor 的 ReAct 循环写 200 行 Agent,只用 TaoToken 发的 Key 行不行?先给结论:行,但你要把 Key 放在模型调用这一层,而不是指望它参与循环。Cursor 那套骨架拆开看很朴素:LLM 当大脑,工具当手,一个 for 循环当心跳。长会话里每一轮都要把 messages 整段发回模型,Token 就是在一次次 tool_calls 往返里消耗掉的。你真正要先定的是请求发去哪一层:本地 agent.py 负责循环、文件读写、命令执行和上下文裁剪,TaoToken 只提供 Key 和 Base URL。打开官网创建 Key,把调用里的 Base URL 写成 https://taotoken.net/api,剩下循环逻辑一行都不用交给它。

这个判断很重要,因为很多人第一次写 Agent 会把“模型服务”和“Agent 运行时”混在一起。模型服务负责根据当前 messages 判断下一步要不要调工具;Agent 运行时负责执行工具、把结果塞回消息数组、决定什么时候停。Cursor 看起来像黑魔法,核心也是这个循环,只是它把工具链、上下文管理、diff 预览、错误重试做得更工程化。你自己写的 200 行版本不需要一开始就追平 Cursor,先把 ReAct 循环跑通,再逐步补上下文压缩和工具安全。

所以本文不讨论怎么把 TaoToken 塞进循环逻辑,而是讨论怎么让 agent.py 通过 TaoToken 发请求。你最后会得到三个东西:一个可复制的 Python 骨架、一套从零建 React Todo 的验证流程、一份常见报错排查表。适合正在写 Agent、Harness、长会话编程助手的人,也适合想理解 Cursor 原理但不想只看概念图的人。下面按“先定边界,再写循环,再验证请求,最后排障”的顺序来。

2. TaoToken 前置:Key、Base URL 与长会话 Agent 的边界

2.1 你只需要拿到 Key 和 Base URL

TaoToken 在这个方案里的角色很明确:发 Key,发 Base URL,不参与你的 ReAct 循环,也不负责读写文件。也就是说,你不需要把工具执行结果上传到某个“Agent 托管层”,更不需要让外部服务碰你的本地目录。你的 agent.py 仍然是唯一的心跳:它拼 messages、发请求、解析 tool_calls、执行工具、把结果塞回 messages,然后进入下一轮。

第一次接入时,建议按这个顺序走:先从官网进入控制台,创建 API Key,再到 API Keys 页面确认 Key 可用,同时在模型列表或接入文档里复制一个支持工具调用的模型 ID。Base URL 固定写https://taotoken.net/api,不要自己拼/v1/chat/completions,OpenAI SDK 会自动补路径。如果你在接入文档里看到示例,优先按示例的模型名和参数格式来。

官网入口可以走这里: TaoToken 官网

创建 Key 和查接入文档: API Keys 接入文档

2.2 准备目录与依赖

先建一个干净目录,别一上来就在真实项目根目录跑 Agent。工具会写文件、执行命令,工作区隔离是最低成本的保险。目录结构建议这样:

agent-demo/ agent.py .env workspace/

安装依赖和创建虚拟环境:

mkdir -p agent-demo/workspace cd agent-demo python -m venv .venv source .venv/bin/activate pip install openai python-dotenv

Windows 下激活命令换成:

.venv\Scripts\activate

.env里放两个变量,不要把 Key 写进代码:

TAOTOKEN_API_KEY=你的Key TAOTOKEN_MODEL=控制台里支持工具调用的模型ID

代码里通过环境变量读取,后面换模型只改.env,不用动循环骨架。注意:Key 不要提交到 Git,也不要在日志里打印完整值。需要检查时只打印前几位或长度。

3. 可复制配置:agent.py 里接上 https://taotoken.net/api

3.1 客户端初始化和工具 schema

核心只有一句:base_url="https://taotoken.net/api"。下面这段是 Agent 的模型调用层,循环逻辑仍然在你本地。先写导入、客户端、工作区限制和工具定义。

import os import json import subprocess from pathlib import Path from dotenv import load_dotenv from openai import OpenAI load_dotenv() client = OpenAI( api_key=os.environ["TAOTOKEN_API_KEY"], base_url="https://taotoken.net/api", ) MODEL = os.environ["TAOTOKEN_MODEL"] WORKSPACE = Path("./workspace").resolve() WORKSPACE.mkdir(parents=True, exist_ok=True) MAX_STEPS = 12 TOOLS = [ { "type": "function", "function": { "name": "list_dir", "description": "列出 workspace 内某个目录的文件和子目录,path 用相对路径,默认 .", "parameters": { "type": "object", "properties": {"path": {"type": "string"}}, "required": [], }, }, }, { "type": "function", "function": { "name": "read_file", "description": "读取 workspace 内的文本文件内容,path 是相对路径。", "parameters": { "type": "object", "properties": {"path": {"type": "string"}}, "required": ["path"], }, }, }, { "type": "function", "function": { "name": "write_file", "description": "写入文本文件到 workspace,目录不存在会自动创建。", "parameters": { "type": "object", "properties": { "path": {"type": "string"}, "content": {"type": "string"}, }, "required": ["path", "content"], }, }, }, { "type": "function", "function": { "name": "run_cmd", "description": "在 workspace 下执行 shell 命令,返回退出码和输出。", "parameters": { "type": "object", "properties": {"command": {"type": "string"}}, "required": ["command"], }, }, }, ]

工具描述不要写太长,但要写清楚边界。容易混淆的参数写详细,比如path明确是相对 workspace 的路径。写太长会占上下文,长会话里每轮都要带上这些描述,Token 消耗会上去。

3.2 工具执行器与安全路径

工具执行器是 Agent 的“手”。这里加两个基本保护:路径不能越出 workspace,命令不能包含明显高危片段。生产环境还应该做白名单,不要直接开放任意 shell。

def safe_path(rel: str) -> Path: p = (WORKSPACE / rel).resolve() if not str(p).startswith(str(WORKSPACE)): raise ValueError("路径越界,只能访问 workspace 内文件") return p def tool_list_dir(args): path = args.get("path", ".") p = safe_path(path) if not p.exists(): return f"目录不存在:{path}" items = [] for child in sorted(p.iterdir()): mark = "/" if child.is_dir() else "" items.append(child.name + mark) return "\n".join(items) if items else "空目录" def tool_read_file(args): p = safe_path(args["path"]) if not p.exists(): return f"文件不存在:{args['path']}" text = p.read_text(encoding="utf-8", errors="replace") return text[:8000] def tool_write_file(args): p = safe_path(args["path"]) p.parent.mkdir(parents=True, exist_ok=True) p.write_text(args["content"], encoding="utf-8") return f"已写入 {args['path']},共 {len(args['content'])} 字符" def tool_run_cmd(args): cmd = args["command"].strip() danger = ["rm -rf /", "mkfs", ":(){", "shutdown", "reboot"] if any(d in cmd for d in danger): return "拒绝执行高危命令" r = subprocess.run( cmd, shell=True, cwd=WORKSPACE, capture_output=True, text=True, timeout=180, ) out = (r.stdout + r.stderr)[-4000:] return f"exit={r.returncode}\n{out}" TOOL_MAP = { "list_dir": tool_list_dir, "read_file": tool_read_file, "write_file": tool_write_file, "run_cmd": tool_run_cmd, }

这里有个常见坑:命令已经在cwd=WORKSPACE下执行,所以 system prompt 里要明确告诉模型,需要进子目录时在一条命令里写cd todo-app && npm install,不要既写cd又幻想有个不存在的workingDirectory参数。否则模型很容易在同一轮里重复切换目录,把命令跑歪。

3.3 ReAct 循环体

循环体是整个 Agent 的心跳。每一轮:发请求,看模型是否返回 tool_calls;没有 tool_calls 就结束;有 tool_calls 就逐个执行,把结果塞回 messages,再进入下一轮。注意 assistant 消息和 tool 消息要配对,tool_call_id必须一致。

SYSTEM_PROMPT = """你是一个在本地 workspace 里工作的编程 Agent。 规则: 1. 只能使用提供的工具,所有路径都是相对 workspace 的路径。 2. 写文件前先 list_dir 或 read_file,不要凭空猜文件内容。 3. 命令默认已经在 workspace 下执行,需要进子目录时用 cd xxx && 命令。 4. 每轮只做一件小事,拿到工具结果后再决定下一步。 5. 完成后直接输出总结,不要继续调用工具。 """ def run_agent(task: str): messages = [ {"role": "system", "content": SYSTEM_PROMPT}, {"role": "user", "content": task}, ] for step in range(1, MAX_STEPS + 1): resp = client.chat.completions.create( model=MODEL, messages=messages, tools=TOOLS, tool_choice="auto", temperature=0, ) msg = resp.choices[0].message assistant_msg = {"role": "assistant", "content": msg.content or ""} if msg.tool_calls: assistant_msg["tool_calls"] = [ { "id": tc.id, "type": "function", "function": { "name": tc.function.name, "arguments": tc.function.arguments, }, } for tc in msg.tool_calls ] messages.append(assistant_msg) if not msg.tool_calls: print(f"[step {step}] finish") print(msg.content) return msg.content for tc in msg.tool_calls: name = tc.function.name args = json.loads(tc.function.arguments or "{}") print(f"[step {step}] {name} {args}") try: result = TOOL_MAP[name](args) except Exception as e: result = f"工具执行失败:{e}" messages.append({ "role": "tool", "tool_call_id": tc.id, "content": str(result), }) print("达到最大步数,任务未完成") return "达到最大步数" if __name__ == "__main__": import sys task = sys.argv[1] if len(sys.argv) > 1 else "在 workspace 创建一个 hello.txt,内容写 hello agent" run_agent(task)

这段不到 200 行,已经包含 Cursor 式 ReAct 的核心:绑定工具、模型决策、执行工具、结果回填、下一轮思考。temperature=0建议保留,写代码要稳定,不要让它自由发挥。工具结果必须原封不动塞回去,否则模型下一轮会“失忆”,反复调用同一个工具。

4. 验证请求:从零建 React Todo,七轮循环看 tool_calls

4.1 设置环境变量并启动

先把.env填好,然后激活虚拟环境,给 Agent 派一个多步骤任务。为了验证长会话循环,任务不要太小,最好包含建项目、写文件、装依赖、构建验证。下面这条命令会跑 Vite React 项目,但不会常驻启动 dev server。

export TAOTOKEN_API_KEY="你的Key" export TAOTOKEN_MODEL="控制台里支持工具调用的模型ID" python agent.py "在 workspace 里用 Vite 创建一个 React TodoList。步骤:1 执行 npm create vite@latest todo-app -- --template react;2 进入 todo-app,改写 src/App.jsx 实现增删改查、筛选、localStorage;3 写必要的 CSS;4 执行 npm install;5 执行 npm run build 验证构建成功。不要启动常驻 dev server。"

Windows PowerShell 用:

$env:TAOTOKEN_API_KEY="你的Key" $env:TAOTOKEN_MODEL="控制台里支持工具调用的模型ID" python agent.py "同上任务"

4.2 观察日志和成功结果

正常情况下你会看到类似这样的轮次:

[step 1] run_cmd {'command': 'npm create vite@latest todo-app -- --template react'} [step 2] list_dir {'path': 'todo-app'} [step 3] read_file {'path': 'todo-app/src/App.jsx'} [step 4] write_file {'path': 'todo-app/src/App.jsx', 'content': '...'} [step 5] write_file {'path': 'todo-app/src/App.css', 'content': '...'} [step 6] run_cmd {'command': 'cd todo-app && npm install'} [step 7] run_cmd {'command': 'cd todo-app && npm run build'} [step 7] finish

成功时npm run build的退出码应为 0,workspace 下会出现todo-app/dist。打开todo-app/src/App.jsx,你能看到模型写入的增删改查、筛选和 localStorage 逻辑。这个验证的重点不是 Todo 有多漂亮,而是确认三件事:模型能稳定返回 tool_calls,本地循环能正确执行工具,工具结果能回填后继续下一轮。

实测下来,七轮左右能完成一个可构建的 React Todo。如果模型在第 3 步后就不再调工具,通常是 tools schema 或模型能力问题;如果一直在 read_file 和 list_dir 之间打转,通常是工具结果没有正确塞回 messages。长会话里还要关注 Token 曲线:每轮都会带上历史 messages,工具输出越长,后续每轮越贵。所以run_cmd里截断输出到 4000 字符、read_file截断到 8000 字符,是很有必要的工程习惯。

4.3 长会话下的上下文裁剪

Agent 要跑长任务,不能无限把历史堆进 messages。一个简单策略是保留 system 消息、最近若干轮对话和任务摘要,但不要直接按条数硬切,因为可能把 assistant 的 tool_calls 和后面的 tool 消息切断。更稳的做法是按“轮次”保留,或者只截断工具输出内容。

def trim_tool_output(text: str, limit: int = 4000) -> str: if len(text) <= limit: return text return text[:2000] + "\n...[中间省略]...\n" + text[-1500:]

你可以把工具执行结果先过一遍这个函数,再放进messages。TaoToken 不负责你的上下文管理,这是 Harness 该做的事。真正跑长期编码时,上下文裁剪和工具安全比模型名字更影响体验。

5. 本篇常见错排查:401、404、tool_calls 丢失与历史膨胀

5.1 401 Unauthorized

报错通常长这样:AuthenticationError: 401。先检查环境变量名有没有写错,代码里是TAOTOKEN_API_KEY.env里也必须是同一个。再确认load_dotenv()在创建客户端之前执行。Windows 下如果同时用了系统环境变量和.env,优先级可能和你想的不一样。检查时不要打印完整 Key,打印长度即可:

python -c "import os; print(len(os.getenv('TAOTOKEN_API_KEY','')))"

如果长度是 0,说明没读到。到 API Keys 重新确认 Key,再看 接入文档 的示例格式。

5.2 404 Not Found 或 model not found

最常见原因是 Base URL 写错。正确写法是base_url="https://taotoken.net/api",不要写成https://taotoken.net/api/v1,也不要手动拼/chat/completions。OpenAI SDK 会自己补路径。另一个原因是模型 ID 不对或当前 Key 没有该模型权限。模型 ID 从控制台复制,不要凭记忆写。如果你在模型对话里能正常聊天,但代码里 404,优先检查 base_url 和模型名。

5.3 tool_calls 为空,Agent 不干活

如果模型只返回文本,不返回tool_calls,先看三处:模型是否支持工具调用;tools数组格式是否合法;tool_choice是否写成"auto"。有些模型在中文描述下会犹豫,可以把工具描述写得更直接,例如“必须使用 write_file 写入文件”。如果换了模型还是不行,就换一个明确支持 function calling 的模型。验证模型能力可以去 模型对话 里发一条带 tools 的请求,看它能否稳定返回工具调用。

5.4 死循环、反复调用同一个工具

典型日志是read_fileread_fileread_file不停重复。原因通常是工具结果没有塞回 messages,或者tool_call_id对不上。每次执行完工具,必须追加{"role": "tool", "tool_call_id": tc.id, "content": result}。另外,assistant消息里也要保留对应的tool_calls。少任何一边,模型都会觉得工具没执行,于是继续调。这个错误非常隐蔽,建议在循环里打印len(messages)和每一步的工具名,方便确认配对关系。

5.5 历史膨胀、中途断掉

长任务跑到后面,messages 会越来越大。表现是响应变慢、成本上升、模型开始忽略早期指令。处理方式有三层:工具输出截断;按轮次保留最近 N 轮;把早期任务和目标压缩成一条摘要。不要直接把 messages 切片,因为可能破坏 tool 消息配对。简单做法是只对role == "tool"的消息做内容截断,或者每 5 轮把早期对话总结成一段system补充。如果你准备做长期编码 Agent,可以看 Coding Plan,里面更适合持续多轮的编码场景。

5.6 命令跑飞或路径越界

WORKSPACE设成项目子目录,不要设成用户根目录。run_cmd至少加高危命令拦截,生产环境建议白名单。write_fileread_file必须走safe_path。另外,模型很擅长写“看起来对”的命令,但不擅长处理交互式命令。像npm create vite这种可能交互的脚手架,要加-- --template react让它非交互执行。否则命令卡住,Agent 会以为工具失败,然后反复重试。

6. 语义一致 CTA:Agent 接入排障、模型验证与长期编码怎么选

把 Key 放对环境变量,把base_url写成https://taotoken.net/api,再让 agent.py 自己跑 for 循环,这套 200 行 ReAct 骨架就能持续跑多轮。TaoToken 只发 Key 和 Base URL,循环逻辑、工具执行、文件读写、上下文裁剪都在你本地。你不需要改 Cursor 式 ReAct 的“绑定工具、执行工具、结果回填、下一轮思考”这四步,只改模型调用这一处。

如果你现在卡在 401、404 或 tool_calls 为空,先去 API Keys 确认 Key,再对照 接入文档 检查参数。只是想验证模型能不能稳定返回工具调用,去 模型对话 发一条最小请求最直接。准备把循环跑成长期编码或 Agent 工作流,再看 Coding Plan 会少走弯路。官网入口在 TaoToken 官网,API 地址直接记https://taotoken.net/api。我自己的习惯是 Key 只放.env,循环只留agent.py,换模型只改两个环境变量,这样 200 行骨架就能一直复用。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询