
1. 项目缘起为什么我们需要一个“编程Agent”最近几年AI编程助手的概念火得一塌糊涂从GitHub Copilot到Cursor再到各种层出不穷的本地模型工具它们确实极大地提升了开发效率。但用久了我总感觉有点“隔靴搔痒”。这些工具要么是云端服务对代码库的全局理解有限响应速度受网络影响要么是本地模型能力又往往局限于代码补全缺乏自主规划和执行复杂任务的能力。我想要的是一个能真正理解我意图、能拆解任务、能调用工具、能自我验证并循环迭代的“智能体”Agent。它应该像一个不知疲倦的初级程序员我只需要给出一个模糊的需求比如“给这个Flask应用加个用户登录功能”它就能自己去查文档、写代码、跑测试、修Bug直到功能可用。这听起来很科幻但基于现有的一些开源工具我们完全可以从零开始搭建一个属于我们自己的、轻量级的“Mini-Cursor”。这个项目的核心就是利用四个关键工具构建一个能够自主循环工作的编程智能体。它不是要替代程序员而是成为一个强大的“副驾驶”处理那些繁琐、重复但又有明确模式的开发任务让我们能更专注于架构设计和核心逻辑。接下来我就带你一步步拆解这个想法看看如何用有限的资源实现一个具备“思考-行动-观察-迭代”能力的编程Agent。2. 核心架构理解“四个工具一个循环”的设计哲学在动手之前我们必须先厘清整个系统的设计思路。所谓“四个工具一个循环”并不是指只能用四个软件而是一种高度抽象的核心组件模型。这个模型确保了Agent的自主性和有效性。四个核心工具分别承担了智能体的不同心智能力“大脑” - 大型语言模型LLM这是Agent的决策核心。它负责理解自然语言指令、拆解任务、规划步骤、生成代码、分析执行结果并决定下一步行动。我们通常需要一个具备较强代码理解和生成能力的模型例如DeepSeek-Coder、CodeLlama或Qwen-Coder系列。它的提示词Prompt工程是灵魂决定了Agent的“性格”和能力边界。“手” - 代码执行器Code ExecutorAgent不能只“空想”必须能“动手”。这个工具负责在安全的沙箱环境中运行生成的代码片段如单个函数、脚本并捕获输出、错误信息以及执行状态成功/失败。Python的subprocess、exec或者更安全的Docker容器、Firejail沙箱都是可选方案。关键在于隔离性与反馈的完整性。“眼” - 文件系统观察器File System WatcherAgent需要感知环境变化。当它修改了项目文件后需要能“看到”修改的内容以便进行自我验证或作为下一步决策的输入。例如它写了一个新的config.py文件后续生成测试代码时就需要读取这个文件的内容。watchdogPython库或操作系统的文件事件监听机制可以充当这双“眼睛”。“记忆与工作台” - 项目上下文管理器Project Context ManagerAgent不能失忆。它需要维护一个持久的“工作记忆”包括原始用户需求、已完成的步骤、当前代码库的状态如关键文件的内容、历史执行结果和错误日志。这通常通过一个结构化的数据库如SQLite、向量数据库如Chroma用于代码片段检索或者简单地维护一个精心设计的JSON状态文件来实现。一个循环指的是驱动Agent运行的ReActReasoning and Acting循环。这不是一个工具而是一个工作流程引擎。其核心步骤是思考ThinkLLM根据当前任务状态和上下文分析下一步应该做什么。例如“用户要求添加登录功能。我已创建了用户模型。下一步需要创建登录视图函数。”行动ActLLM生成具体的行动指令通常是调用一个工具Tool。例如“调用write_file工具在app/auth.py中写入登录视图函数代码。”观察Observe系统执行该行动如写入文件并收集结果如文件写入成功或执行代码后返回了错误ImportError。更新Update将观察到的结果反馈给LLM更新上下文。然后循环回到第1步“思考”。这个循环会一直持续直到LLM判断任务已经完成生成最终答案或者达到最大迭代次数、遇到无法解决的错误为止。整个系统的架构图可以想象成LLM作为中央处理器不断与另外三个工具执行器、观察器、上下文管理器进行交互形成一个闭环。3. 工具选型与实战配置搭建我们的智能体工作台理论清晰后我们开始动手选型和配置。这里我会给出一个基于Python技术栈的、高性价比的实操方案。你可以根据自己的偏好替换其中的组件。3.1 “大脑”的安装与接入本地LLM服务化云端API如OpenAI GPT-4、Claude虽然强大但考虑到成本、延迟和对代码库的隐私性我们优先选择本地部署的开源模型。方案选择Ollama DeepSeek-CoderOllama是目前最方便的本地大模型运行和管理的工具它简化了模型下载、加载和提供API的全过程。操作步骤安装Ollama前往官网https://ollama.com根据你的操作系统Windows/macOS/Linux下载安装。拉取代码模型打开终端运行ollama pull deepseek-coder:6.7b。这里选择6.7B参数的版本在大多数消费级显卡如RTX 3060 12GB上可以流畅运行且代码能力足够强。如果你的硬件更强可以尝试deepseek-coder:33b。运行模型服务ollama run deepseek-coder:6.7b会进入交互模式。但我们更需要它的API服务。通过ollama serve命令Ollama会在本地11434端口启动一个兼容OpenAI API格式的接口。这样我们的Agent程序就可以像调用ChatGPT一样调用本地模型了。关键配置与验证# 测试Ollama API是否通畅 import openai # 需要安装openai库 client openai.OpenAI( base_urlhttp://localhost:11434/v1, api_keyollama, # ollama的API key可以任意填写非空即可 ) response client.chat.completions.create( modeldeepseek-coder:6.7b, messages[{role: user, content: 用Python写一个快速排序函数。}] ) print(response.choices[0].message.content)如果能成功打印出代码说明你的“大脑”已就位。注意首次运行或切换模型时Ollama需要从硬盘加载模型到显存可能会有几十秒的等待时间这是正常的。确保你的系统有足够的GPU内存或大的交换空间Swap。3.2 “手”的锻造安全且反馈详细的代码执行器让AI直接在你的主系统上运行代码是极其危险的。我们必须构建一个沙箱。方案选择Docker容器作为执行沙箱Docker能提供完美的环境隔离和资源限制。我们预先准备一个包含项目所需基础环境如Python, Node.js的镜像。操作步骤创建Dockerfile在项目根目录创建Dockerfile.executor。FROM python:3.11-slim WORKDIR /workspace # 复制当前项目代码到容器内在运行时动态绑定挂载更灵活此处为示例 COPY . . # 可以预先安装一些常用库 RUN pip install --no-cache-dir pytest black isort CMD [tail, -f, /dev/null] # 保持容器运行构建镜像docker build -f Dockerfile.executor -t code-executor .在Agent中调用我们的Agent需要能动态创建容器、复制代码进去、执行命令、获取结果并清理容器。核心执行函数示例import docker import tempfile import os class DockerCodeExecutor: def __init__(self): self.client docker.from_env() self.image_name code-executor def execute_code(self, code: str, command: str python -c) - dict: 在Docker容器中执行一段代码或命令。 :param code: 要执行的代码字符串 :param command: 执行命令如 python -c 或 bash -c :return: 包含 stdout, stderr, returncode 的字典 # 创建临时目录存放代码文件 with tempfile.TemporaryDirectory() as tmpdir: code_path os.path.join(tmpdir, script.py) with open(code_path, w) as f: f.write(code) # 创建并运行容器 container self.client.containers.run( self.image_name, f{command} {code}, volumes{tmpdir: {bind: /tmp/workspace, mode: ro}}, working_dir/tmp/workspace, detachTrue, stdoutTrue, stderrTrue, removeFalse # 不自动删除方便查看日志 ) # 等待执行完成并获取日志 result container.wait() stdout container.logs(stdoutTrue, stderrFalse).decode() stderr container.logs(stdoutFalse, stderrTrue).decode() container.remove() # 清理容器 return { stdout: stdout, stderr: stderr, returncode: result[StatusCode], success: result[StatusCode] 0 }这个执行器不仅运行代码还捕获了成功/失败状态以及完整的输出和错误信息这些信息对于LLM进行下一步“思考”至关重要。3.3 “眼”与“记忆”的实现上下文感知与状态持久化文件观察和上下文管理相对轻量我们可以用Python库和简单的数据结构来实现。文件系统观察器使用watchdogfrom watchdog.observers import Observer from watchdog.events import FileSystemEventHandler import time class CodeChangeHandler(FileSystemEventHandler): def on_modified(self, event): if not event.is_directory and event.src_path.endswith(.py): print(f检测到文件变更: {event.src_path}) # 这里可以触发一个回调通知Agent上下文管理器更新文件内容缓存 # 在Agent主循环中启动观察者 observer Observer() observer.schedule(CodeChangeHandler(), path./your_project, recursiveTrue) observer.start() # 主循环结束后 observer.stop()在实际的Agent中我们不一定需要实时监听。更简单的策略是在每次ReAct循环的“观察”阶段主动去读取被修改文件的最新内容更新到上下文中。项目上下文管理器一个增强的Python类我们设计一个ProjectContext类来充当Agent的“工作记忆”。import json import os from pathlib import Path class ProjectContext: def __init__(self, project_root: str): self.root Path(project_root) self.original_task # 原始用户需求 self.history [] # 记录每一步的思考、行动、观察 self.file_cache {} # 缓存关键文件内容避免频繁IO self.current_state idle # 任务状态planning, coding, testing, debugging, done def update_file_cache(self, file_path: str): 更新单个文件的缓存 full_path self.root / file_path if full_path.exists(): with open(full_path, r, encodingutf-8) as f: self.file_cache[file_path] f.read() else: # 文件可能被删除 self.file_cache.pop(file_path, None) def get_relevant_context(self, current_step: str) - str: 根据当前步骤组装相关的上下文信息作为LLM Prompt的一部分。 这是提升Agent表现的关键 context_lines [] context_lines.append(f原始任务: {self.original_task}) context_lines.append(f当前任务阶段: {current_step}) context_lines.append(最近几步操作历史:) for h in self.history[-5:]: # 只保留最近5步防止Token过长 context_lines.append(f- Think: {h.get(think)}) context_lines.append(f- Act: {h.get(act)}) context_lines.append(f- Observe: {h.get(observe)}) context_lines.append(\n当前项目关键文件内容:) for file, content in list(self.file_cache.items())[-3:]: # 缓存最近3个相关文件 context_lines.append(f--- File: {file} ---) context_lines.append(content[:500] ... if len(content) 500 else content) # 截断长文件 return \n.join(context_lines) def add_history(self, think: str, act: str, observe: str): self.history.append({think: think, act: act, observe: observe}) # 保存到JSON文件实现持久化 with open(self.root / agent_history.json, w) as f: json.dump({history: self.history, task: self.original_task}, f, indent2)这个上下文管理器负责组织信息它决定了在每一步哪些信息会被送入LLM的“脑海”中。精心设计的get_relevant_context方法能显著提高Agent的任务完成率。4. ReAct循环的工程实现让Agent真正“动”起来有了工具我们需要一个“主循环”来驱动一切。这是整个项目的核心逻辑。4.1 定义Agent可用的工具集Tools首先我们要告诉LLM它能“用手”做什么。我们将工具定义为函数并用一个统一的描述格式来声明。# 定义工具 def write_file(filepath: str, content: str) - str: 将内容写入指定文件路径。如果文件存在则覆盖。 full_path Path(filepath) full_path.parent.mkdir(parentsTrue, exist_okTrue) full_path.write_text(content, encodingutf-8) return f文件 {filepath} 写入成功。 def read_file(filepath: str) - str: 读取指定文件路径的内容。 try: return Path(filepath).read_text(encodingutf-8) except FileNotFoundError: return f错误文件 {filepath} 不存在。 def execute_python_script(code: str) - str: 在隔离环境中执行一段Python代码并返回输出或错误。 result executor.execute_code(code, commandpython -c) if result[success]: return f执行成功。输出\n{result[stdout]} else: return f执行失败。错误\n{result[stderr]} def run_shell_command(cmd: str) - str: 在项目根目录运行一个shell命令如 pip install 或 pytest。 # 注意这里需要在Docker容器内运行或做严格的安全限制 result executor.execute_code(cmd, commandbash -c) return f命令 {cmd} 执行完毕。返回码{result[returncode]}。输出\n{result[stdout]}\n错误\n{result[stderr]} # 工具描述列表用于构造给LLM的Prompt TOOLS [ { name: write_file, description: 将内容写入文件。参数filepath文件路径, content内容。, parameters: [filepath, content] }, { name: read_file, description: 读取文件内容。参数filepath文件路径。, parameters: [filepath] }, { name: execute_python_script, description: 执行一段Python代码并返回结果。参数code代码字符串。, parameters: [code] }, { name: run_shell_command, description: 运行一个shell命令如安装依赖、运行测试。参数cmd命令字符串。, parameters: [cmd] } ]4.2 构造驱动LLM思考与行动的PromptPrompt是Agent的“灵魂指令”。它需要清晰地定义角色、约束、可用工具和输出格式。def build_agent_prompt(task: str, context: str) - str: tools_description \n.join([f- {t[name]}: {t[description]} for t in TOOLS]) return f你是一个自主的编程AI助手Agent。你的目标是通过思考、使用工具、观察结果并循环最终完成用户的任务。 # 当前任务 {task} # 当前项目上下文 {context} # 你可以使用的工具 {tools_description} # 你的工作流程ReAct循环 1. 思考Think分析当前情况和任务决定下一步做什么。如果需要使用工具请明确说明工具名和参数。 2. 行动Act严格按照以下JSON格式调用工具 json {{action: 工具名, args: {{参数1: 值1, 参数2: 值2}}}}观察Observe我会将工具执行的结果返回给你。重复1-3步直到任务完成或无法继续。输出格式要求你的每次回复必须且只能包含一个JSON对象格式如下{{ thought: 你的思考过程解释为什么选择这个行动。, action: {{action: 工具名, args: {{...}}}} // 或者当任务完成时设为 null }}当任务彻底完成时将action设为null并在thought中总结最终成果。现在开始你的第一次思考。 这个Prompt明确要求LLM以固定的JSON格式回复这极大简化了后续的解析逻辑使得程序能够稳定地从LLM的输出中提取“思考”和“行动”。 ### 4.3 实现主循环控制器 最后我们将所有部分串联起来形成主循环。 python import json import re class ProgrammingAgent: def __init__(self, llm_client, context: ProjectContext, max_steps20): self.llm llm_client self.ctx context self.max_steps max_steps self.tools_map { write_file: write_file, read_file: read_file, execute_python_script: execute_python_script, run_shell_command: run_shell_command, } def run(self, task: str): self.ctx.original_task task print(f开始处理任务: {task}) step 0 while step self.max_steps: step 1 print(f\n 步骤 {step} ) # 1. 获取当前上下文构建Prompt current_context self.ctx.get_relevant_context(fstep_{step}) prompt build_agent_prompt(task, current_context) # 2. 调用LLM进行“思考” response self.llm.chat.completions.create( modeldeepseek-coder:6.7b, messages[{role: user, content: prompt}], temperature0.1 # 低温度保证输出稳定减少随机性 ) llm_output response.choices[0].message.content print(fLLM原始输出:\n{llm_output}) # 3. 解析LLM的回复 try: # 使用正则表达式提取JSON部分提高容错性 json_match re.search(rjson\s*(.*?)\s*, llm_output, re.DOTALL) if json_match: json_str json_match.group(1) else: json_str llm_output.strip() agent_response json.loads(json_str) except json.JSONDecodeError as e: print(f解析LLM输出失败: {e}) self.ctx.add_history(LLM返回了无法解析的格式, llm_output, 解析错误) break thought agent_response.get(thought, ) action_data agent_response.get(action) # 4. 判断是否结束 if action_data is None: print(f任务完成最终思考: {thought}) self.ctx.add_history(thought, FINISH, 任务完成) break # 5. 执行“行动” action_name action_data[action] args action_data[args] print(f思考: {thought}) print(f执行动作: {action_name} 参数: {args}) if action_name not in self.tools_map: observe f错误未知工具 {action_name}。 else: try: tool_func self.tools_map[action_name] # 动态调用工具函数 observe tool_func(**args) except Exception as e: observe f工具执行异常: {e} print(f观察结果: {observe}) # 6. “更新”上下文记录历史并更新文件缓存如果涉及文件操作 self.ctx.add_history(thought, f{action_name}({args}), observe) if action_name write_file: self.ctx.update_file_cache(args[filepath]) # 可选如果执行的是读文件操作将内容也主动更新到上下文中 if action_name read_file: # 假设read_file返回的是文件内容 self.ctx.file_cache[args[filepath]] observe if 错误 not in observe else if step self.max_steps: print(f达到最大步数限制 ({self.max_steps})任务未完成。) # 启动Agent if __name__ __main__: llm_client openai.OpenAI(base_urlhttp://localhost:11434/v1, api_keyollama) context ProjectContext(./my_python_project) executor DockerCodeExecutor() agent ProgrammingAgent(llm_client, context) agent.run(在项目根目录创建一个名为 utils.py 的文件并在其中编写一个函数 calculate_average(numbers: list) - float用于计算列表的平均值。然后写一个简单的测试脚本来验证它。)这个主循环清晰地体现了ReAct的流程构建Prompt - LLM思考并计划行动 - 解析并执行行动 - 观察结果 - 记录并进入下一轮。5. 实战演练与效果调优让Agent完成真实任务让我们用一个更复杂的任务来测试这个Mini-Cursor。假设我们有一个简单的Flask项目骨架现在要求Agent为其添加一个简单的RESTful API端点。初始项目结构my_flask_project/ ├── app.py (已有基础Flask app) ├── requirements.txt (已有Flask依赖) └── ...给Agent的任务指令“在现有的Flask应用中添加一个/api/books的GET端点返回一个固定的图书列表JSON例如[{id: 1, title: Python编程}]。请确保代码符合Flask规范。”Agent的执行过程推演简化版步骤1LLM思考后决定先读取app.py了解现有结构。调用read_file工具。观察获取到现有app.py内容。上下文更新。步骤2LLM思考决定在app.py中添加新的路由。调用write_file工具写入修改后的完整app.py代码。观察文件写入成功。上下文更新文件缓存刷新。步骤3LLM思考为了验证端点是否工作需要启动服务并测试。但启动服务是阻塞操作。LLM可能决定先写一个简单的测试脚本。调用write_file工具创建test_api.py使用requests库测试本地端点。观察测试文件创建成功。步骤4LLM思考需要安装requests库如果未安装。调用run_shell_command工具执行pip install requests。观察安装成功。步骤5LLM思考运行测试脚本。调用execute_python_script工具运行test_api.py。观察测试失败因为Flask应用没有运行。错误信息被捕获。步骤6LLM思考需要先启动Flask应用在后台再运行测试。这涉及到进程管理可能超出当前工具能力。LLM可能会在思考中表示无法完成或者尝试更复杂的方案如使用subprocess启动服务。最终可能因为复杂度而停止或给出需要人工干预的建议。从演练中看到的调优点工具能力的增强我们的工具集还比较基础。可以增加start_flask_server、curl_api等更高级、更贴合特定场景的工具降低LLM规划路径的难度。Prompt工程的优化在Prompt中更明确地引导。例如加入“如果涉及启动Web服务器请先编写测试代码然后提示用户手动启动服务器后再运行测试。” 让Agent学会在边界处与用户协作。上下文的精炼随着步骤增多历史记录和文件缓存会膨胀导致Prompt超长。需要实现更智能的上下文摘要Summarization功能只保留最关键的信息。错误处理的引导当工具执行失败时观察结果错误信息需要被充分结构化并提示LLM如何分析。例如在execute_python_script的返回中可以固定格式“STDOUT: ...\nSTDERR: ...\nERROR_TYPE: ImportError(如果可识别)”。这能帮助LLM更好地“诊断”问题。6. 避坑指南与进阶思考从玩具到可用的关键一跃构建出能跑通的Demo只是第一步。要让这个Mini-Cursor真正有用还需要解决一系列工程化问题。坑一LLM的“幻觉”与不稳定输出即使要求输出JSONLLM有时也会在JSON前后添加多余的解释文字导致解析失败。解决方案像上面代码一样使用正则表达式re.search(rjson\s*(.*?)\s*, ...)进行提取这比单纯json.loads健壮得多。同时在Prompt中强烈强调“必须且只能包含一个JSON对象”。坑二无限循环与原地打转Agent可能会陷入死循环比如反复修改同一行代码或者在“写文件-读文件-再写文件”中循环。解决方案设置最大步数这是最后的防线。在上下文中检测循环在ProjectContext.add_history中检查最近N步的(think, act)组合是否重复出现如果重复则中断并提示。Prompt引导在Prompt中加入“请避免重复执行完全相同的操作。如果遇到错误请尝试新的解决方案而不是重复失败的操作。”坑三安全性与破坏性操作虽然用了Docker沙箱但write_file工具可以覆盖任何项目文件run_shell_command更是危险。解决方案实施路径白名单限制write_file和read_file只能操作项目根目录下的文件。命令黑名单/白名单对run_shell_command执行的命令进行严格过滤禁止rm、format、dd等危险命令或只允许pip install、pytest等少数安全命令。操作确认人工在环对于关键文件如app.py的写入或安装依赖等操作可以设计为Agent生成建议等待用户确认后再执行。进阶思考如何提升Agent的智能更丰富的工具库集成Git操作clone, commit, diff、数据库操作、调用外部API等工具让Agent能力更强。分层规划与子任务分解让LLM先输出一个高层计划如1. 分析项目结构2. 修改主应用文件3. 编写测试4. 运行验证再将每个步骤展开为具体的ReAct循环。这能处理更复杂的任务。集成专业代码知识将项目代码库建立向量索引用Chroma 代码分割当Agent需要了解项目特定逻辑时可以优先检索相关代码片段作为上下文而不是盲目猜测。多Agent协作可以设计“架构师Agent”、“开发Agent”、“测试Agent”角色让它们通过共享上下文进行协作模拟真实的开发流程。构建一个实用的编程Agent是一个持续的迭代过程。从这个“四个工具一个循环”的最小可行产品MVP出发你可以根据实际需求不断打磨Prompt、增加工具、优化上下文管理。它最终会成为你编程工作流中一个极具潜力的助手帮你承担起那些脉络清晰但细节繁琐的“体力活”让你能更聚焦于创造性的部分。