首页
学习
活动
专区
圈层
工具
发布
社区首页 >专栏 >Agent工程化实战:从零构建可扩展的AI编程助手

Agent工程化实战:从零构建可扩展的AI编程助手

原创
作者头像
用户12608867
发布2026-08-29 11:44:00
发布2026-08-29 11:44:00
610
举报

Agent工程化实战:从零构建可扩展的AI编程助手

在LLM爆发式增长的今天,AI Agent早已不再是学术实验室里的玩具。但真正的工程化落地——高可用、可观测、可扩展、可维护——依然是绝大多数团队面临的鸿沟。本文不空谈概念,而是带你从零构建一个生产级AI编程助手Agent,深度拆解工具调用、记忆管理、规划引擎、错误恢复、并发控制等工程化核心模块,全部代码均可直接运行。


一、为什么需要“工程化”Agent?

一个简单的while循环调用LLM API也能称为Agent,但线上运行时你会遇到:

  • 工具调用超时导致整个流程阻塞
  • 上下文膨胀撑爆token窗口
  • 模型陷入死循环不断重试
  • 并发请求互相干扰状态
  • 无结构化日志,故障无法复盘

工程化的目标不是“跑通Demo”,而是可预测、可观测、可治理。我们以“AI编程助手”为例——它需要读取文件、执行Shell命令、搜索代码、编辑并验证——这正是Multi-step Agent的典型场景。


二、整体架构设计

我们采用分层控制 + 事件驱动的架构:

代码语言:javascript
复制
┌─────────────────────────────────────────────────────┐
│                    Orchestrator                      │
│  (规划器 + 执行器 + 反思器)                         │
└────────────┬────────────────────────────────────────┘
             │
     ┌───────┼───────┬──────────────┐
     ▼       ▼       ▼              ▼
┌─────────┐ ┌─────────┐ ┌─────────┐ ┌─────────┐
│ Tool    │ │ Memory  │ │ Logger  │ │ Rate    │
│ Registry│ │ Manager │ │ & Tracer│ │ Limiter │
└─────────┘ └─────────┘ └─────────┘ └─────────┘
  • Orchestrator:维护循环状态,决定下一步动作(规划→执行→观察→反思)。
  • Tool Registry:所有可调用工具(Python函数、Shell命令、API调用)的注册与校验。
  • Memory Manager:短期对话记忆 + 长期向量检索记忆 + 工作区状态缓存。
  • Logger & Tracer:结构化日志(JSON Lines) + OpenTelemetry追踪。
  • Rate Limiter:基于令牌桶的并发和QPS控制。

三、核心模块代码实现

3.1 工具系统(Tool Registry)

工具是Agent的“手脚”。我们要求每个工具声明输入输出Schema(JSON Schema),便于LLM进行Function Calling,同时便于参数校验。

代码语言:javascript
复制
# tool_base.py
from pydantic import BaseModel, Field
from typing import Callable, Dict, Any, Optional, Type
import json
import inspect

class ToolSchema(BaseModel):
    name: str
    description: str
    parameters: Dict[str, Any]  # JSON Schema

class Tool:
    def __init__(
        self,
        name: str,
        description: str,
        func: Callable,
        schema: Optional[Dict[str, Any]] = None,
        timeout: float = 30.0
    ):
        self.name = name
        self.description = description
        self.func = func
        self.timeout = timeout
        self.schema = schema or self._infer_schema(func)

    def _infer_schema(self, func):
        sig = inspect.signature(func)
        params = {}
        for p_name, p_param in sig.parameters.items():
            if p_name == "self" or p_name == "kwargs":
                continue
            param_type = "string"
            if p_param.annotation is not inspect.Parameter.empty:
                if p_param.annotation == int:
                    param_type = "integer"
                elif p_param.annotation == float:
                    param_type = "number"
                elif p_param.annotation == bool:
                    param_type = "boolean"
                elif p_param.annotation == list:
                    param_type = "array"
            params[p_name] = {"type": param_type}
        return {
            "type": "object",
            "properties": params,
            "required": list(params.keys())
        }

    async def execute(self, **kwargs) -> Any:
        # 超时控制
        import asyncio
        try:
            return await asyncio.wait_for(
                asyncio.to_thread(self.func, **kwargs),
                timeout=self.timeout
            )
        except asyncio.TimeoutError:
            raise TimeoutError(f"Tool {self.name} execution timeout")

class ToolRegistry:
    def __init__(self):
        self._tools: Dict[str, Tool] = {}

    def register(self, tool: Tool):
        self._tools[tool.name] = tool

    def get_tool(self, name: str) -> Optional[Tool]:
        return self._tools.get(name)

    def list_schemas(self):
        return [
            {
                "type": "function",
                "function": {
                    "name": t.name,
                    "description": t.description,
                    "parameters": t.schema
                }
            }
            for t in self._tools.values()
        ]

3.2 记忆管理器(Memory Manager)

记忆分为三层:

  • 短期记忆:滑动窗口的对话消息列表(保留最近N轮)。
  • 工作记忆:当前任务上下文(文件路径、环境变量等)。
  • 长期记忆:向量数据库(Chroma)存储历史经验,按需检索。

代码语言:javascript
复制
# memory.py
import chromadb
from chromadb.utils import embedding_functions
from typing import List, Dict, Any
import json
from collections import deque

class MemoryManager:
    def __init__(self, max_short_term=20, collection_name="agent_memory"):
        self.short_term = deque(maxlen=max_short_term)  # 消息列表
        self.working_memory = {}  # 当前任务状态
        # 长期向量存储
        self.client = chromadb.PersistentClient(path="./chroma_db")
        self.embed_fn = embedding_functions.DefaultEmbeddingFunction()
        self.collection = self.client.get_or_create_collection(
            name=collection_name,
            embedding_function=self.embed_fn
        )

    def add_short_term(self, role: str, content: str):
        self.short_term.append({"role": role, "content": content})

    def get_short_term(self) -> List[Dict]:
        return list(self.short_term)

    def add_long_term(self, text: str, metadata: Dict = None):
        # 生成唯一ID
        import uuid
        doc_id = str(uuid.uuid4())
        self.collection.add(
            documents=[text],
            metadatas=[metadata or {}],
            ids=[doc_id]
        )

    def search_long_term(self, query: str, top_k=3) -> List[str]:
        results = self.collection.query(query_texts=[query], n_results=top_k)
        return results['documents'][0] if results['documents'] else []

    def update_working(self, key: str, value: Any):
        self.working_memory[key] = value

    def get_working(self, key: str, default=None):
        return self.working_memory.get(key, default)

3.3 规划引擎(Planner)

我们采用ReAct(Reason+Act)的变体,但增加了显式的反思步骤。每次循环:

  1. Plan:LLM生成一个包含thoughtaction的JSON对象。
  2. Act:执行工具,获得observation
  3. Reflect:基于观察结果,LLM决定是否继续或输出最终答案。

为了避免无限循环,我们设置最大步数(如10步),并使用状态机控制。

代码语言:javascript
复制
# planner.py
import json
import openai
from typing import List, Dict, Any, Optional
from tool_base import ToolRegistry
from memory import MemoryManager

class ReActPlanner:
    def __init__(
        self,
        llm_client,
        model: str = "gpt-4-turbo",
        max_steps: int = 10,
        tool_registry: Optional[ToolRegistry] = None,
        memory: Optional[MemoryManager] = None
    ):
        self.client = llm_client
        self.model = model
        self.max_steps = max_steps
        self.tools = tool_registry
        self.memory = memory
        self.system_prompt = self._build_system_prompt()

    def _build_system_prompt(self):
        tools_desc = ""
        if self.tools:
            for t in self.tools._tools.values():
                tools_desc += f"- {t.name}: {t.description}\n"
        return f"""You are an AI programming assistant with tool access.
You must respond with a JSON object containing:
- "thought": your reasoning about the current step
- "action": the tool name to call (or "finish" to return final answer)
- "action_input": a dict of parameters for the tool (or final answer string if action is "finish")

Available tools:
{tools_desc}

If you need to finish, action="finish" and action_input is your final answer.
Always think step by step. If a tool fails, try an alternative approach.
"""

    async def step(self, user_query: str) -> Dict:
        # 构建消息上下文
        messages = [
            {"role": "system", "content": self.system_prompt},
        ]
        # 加入短期记忆
        if self.memory:
            short = self.memory.get_short_term()
            messages.extend(short)
        # 加入长期记忆(检索相关)
        if self.memory:
            long_docs = self.memory.search_long_term(user_query, top_k=2)
            if long_docs:
                context = "\n".join(long_docs)
                messages.append({"role": "system", "content": f"Relevant past experiences:\n{context}"})
        # 加入当前用户查询
        messages.append({"role": "user", "content": user_query})

        # 调用LLM,强制JSON输出
        response = self.client.chat.completions.create(
            model=self.model,
            messages=messages,
            response_format={"type": "json_object"},
            temperature=0.2,
        )
        content = response.choices[0].message.content
        try:
            parsed = json.loads(content)
            return parsed
        except json.JSONDecodeError:
            # 容错:尝试提取JSON块
            import re
            match = re.search(r'\{.*\}', content, re.DOTALL)
            if match:
                return json.loads(match.group())
            raise ValueError("LLM did not return valid JSON")

3.4 编排器(Orchestrator)

编排器负责循环调用规划器、执行工具、记录结果、处理异常。

代码语言:javascript
复制
# orchestrator.py
import asyncio
import logging
from typing import Optional
from planner import ReActPlanner
from tool_base import ToolRegistry
from memory import MemoryManager
import time

class Orchestrator:
    def __init__(
        self,
        planner: ReActPlanner,
        tool_registry: ToolRegistry,
        memory: MemoryManager,
        max_retries: int = 2,
    ):
        self.planner = planner
        self.tools = tool_registry
        self.memory = memory
        self.max_retries = max_retries
        self.logger = logging.getLogger("orchestrator")
        self.step_count = 0

    async def run(self, user_query: str) -> str:
        self.step_count = 0
        self.memory.add_short_term("user", user_query)
        # 记录追踪ID
        trace_id = f"trace_{int(time.time()*1000)}"
        self.logger.info({"event": "start", "trace_id": trace_id, "query": user_query})

        # 工作内存初始化
        self.memory.update_working("trace_id", trace_id)
        self.memory.update_working("original_query", user_query)

        final_answer = None
        while self.step_count < self.planner.max_steps:
            self.step_count += 1
            try:
                # 1. 规划
                plan = await self.planner.step(user_query)
                thought = plan.get("thought", "")
                action = plan.get("action", "")
                action_input = plan.get("action_input", {})

                self.logger.info({
                    "event": "plan",
                    "step": self.step_count,
                    "thought": thought,
                    "action": action,
                    "action_input": action_input
                })

                # 2. 判断是否结束
                if action == "finish":
                    final_answer = str(action_input)
                    self.memory.add_short_term("assistant", final_answer)
                    self.logger.info({"event": "finish", "answer": final_answer})
                    break

                # 3. 执行工具
                tool = self.tools.get_tool(action)
                if not tool:
                    observation = f"Error: Tool '{action}' not found."
                else:
                    # 带重试执行
                    for retry in range(self.max_retries + 1):
                        try:
                            result = await tool.execute(**action_input)
                            observation = str(result)
                            break
                        except Exception as e:
                            observation = f"Error (attempt {retry+1}): {str(e)}"
                            if retry == self.max_retries:
                                self.logger.error({"event": "tool_failed", "tool": action, "error": str(e)})
                            await asyncio.sleep(0.5 * (retry+1))  # 退避

                # 4. 将观察写入短期记忆,并更新工作内存
                self.memory.add_short_term("assistant", f"Action: {action}\nObservation: {observation}")
                self.memory.update_working("last_observation", observation)
                self.memory.update_working("last_action", action)

                self.logger.info({
                    "event": "observation",
                    "step": self.step_count,
                    "observation": observation[:200]  # 截断日志
                })

                # 5. 下一轮循环将基于新观察继续规划
                # 将用户查询重置为“继续”,但保留上下文通过short_term传递
                user_query = "Continue with the next step based on the observation."

            except Exception as e:
                self.logger.error({"event": "step_error", "step": self.step_count, "error": str(e)})
                # 错误恢复:向记忆中加入错误,并尝试继续
                self.memory.add_short_term("system", f"Error occurred: {str(e)}. Try an alternative approach.")
                user_query = "Recover from error and try another approach."

        if final_answer is None:
            final_answer = "Maximum steps reached without final answer."
        return final_answer

3.5 具体工具实现(编程助手需要的能力)

我们实现四个核心工具:read_file, write_file, execute_shell, search_code

代码语言:javascript
复制
# tools_impl.py
import os
import subprocess
import glob
from pathlib import Path

def read_file(path: str) -> str:
    """Read the content of a file."""
    with open(path, 'r', encoding='utf-8') as f:
        return f.read()

def write_file(path: str, content: str) -> str:
    """Write content to a file (overwrites if exists)."""
    Path(os.path.dirname(path)).mkdir(parents=True, exist_ok=True)
    with open(path, 'w', encoding='utf-8') as f:
        f.write(content)
    return f"File {path} written successfully."

def execute_shell(command: str, cwd: str = ".") -> str:
    """Execute a shell command and return stdout+stderr."""
    result = subprocess.run(
        command,
        shell=True,
        cwd=cwd,
        capture_output=True,
        text=True,
        timeout=60
    )
    output = result.stdout + result.stderr
    if result.returncode != 0:
        return f"Command failed with code {result.returncode}:\n{output}"
    return output

def search_code(pattern: str, root_dir: str = ".") -> str:
    """Search for a regex pattern in all .py files under root_dir."""
    matches = []
    for filepath in glob.glob(f"{root_dir}/**/*.py", recursive=True):
        try:
            with open(filepath, 'r', encoding='utf-8') as f:
                for i, line in enumerate(f.readlines(), 1):
                    if pattern in line:  # 简单字符串匹配,可改为regex
                        matches.append(f"{filepath}:{i}: {line.strip()[:100]}")
        except Exception:
            continue
    if not matches:
        return "No matches found."
    return "\n".join(matches[:20])  # 限制结果数量

3.6 集成与启动入口

代码语言:javascript
复制
# main.py
import openai
import asyncio
import logging
from tool_base import ToolRegistry, Tool
from tools_impl import read_file, write_file, execute_shell, search_code
from memory import MemoryManager
from planner import ReActPlanner
from orchestrator import Orchestrator

def setup_logging():
    logging.basicConfig(level=logging.INFO)
    # 使用JSON格式的日志(便于采集)
    import json_logging
    json_logging.init_non_web(enable_json=True)

async def main():
    # 1. 初始化LLM客户端(使用OpenAI,或兼容接口)
    client = openai.AsyncOpenAI(
        api_key=os.getenv("OPENAI_API_KEY"),
        base_url=os.getenv("OPENAI_BASE_URL", "https://api.openai.com/v1")
    )

    # 2. 注册工具
    registry = ToolRegistry()
    registry.register(Tool("read_file", "Read content of a file", read_file))
    registry.register(Tool("write_file", "Write content to a file", write_file))
    registry.register(Tool("execute_shell", "Execute a shell command", execute_shell))
    registry.register(Tool("search_code", "Search for a pattern in Python code", search_code))

    # 3. 记忆管理器
    memory = MemoryManager(max_short_term=30)

    # 4. 规划器
    planner = ReActPlanner(
        llm_client=client,
        model="gpt-4-turbo",
        max_steps=8,
        tool_registry=registry,
        memory=memory
    )

    # 5. 编排器
    orchestrator = Orchestrator(planner, registry, memory, max_retries=2)

    # 6. 执行一个实际编程任务
    query = """
    I have a Python project in ./my_project. I need to find all functions that have no docstring,
    and then add a standard docstring to each. The docstring should be:
    \"\"\"Describe what this function does.\"\"\"
    Please don't change any other code. Use search_code to find functions, read_file to read, write_file to modify.
    """
    answer = await orchestrator.run(query)
    print("Final Answer:", answer)

if __name__ == "__main__":
    setup_logging()
    asyncio.run(main())

四、工程化进阶:可观测性与容错

4.1 结构化日志与追踪

上面我们已经植入了self.logger.info({"event": ...}),配合json_logging库可以输出JSON Lines,方便接入ELK或Loki。同时,我们为每次对话生成了trace_id,可以在分布式追踪系统中串联所有步骤。

4.2 限流与并发控制

高并发场景下,我们需要限制LLM API调用和工具执行并发。使用asyncio.Semaphore和令牌桶:

代码语言:javascript
复制
# rate_limiter.py
import asyncio
import time

class TokenBucket:
    def __init__(self, rate: float, capacity: int):
        self.rate = rate  # tokens per second
        self.capacity = capacity
        self.tokens = capacity
        self.last_refill = time.monotonic()
        self._lock = asyncio.Lock()

    async def acquire(self, tokens=1):
        async with self._lock:
            now = time.monotonic()
            elapsed = now - self.last_refill
            self.tokens = min(self.capacity, self.tokens + elapsed * self.rate)
            self.last_refill = now
            if self.tokens >= tokens:
                self.tokens -= tokens
                return True
            return False

# 在Orchestrator.run中,每次调用LLM和工具前 await rate_limiter.acquire()

4.3 状态持久化与恢复

如果Agent在运行中途崩溃,我们可以将short_term记忆和working_memory定期序列化到Redis或磁盘。重启后恢复状态,继续执行。

代码语言:javascript
复制
# 在Orchestrator.run循环中,每隔几步保存检查点
async def _checkpoint(self):
    import pickle
    state = {
        "short_term": list(self.memory.short_term),
        "working": self.memory.working_memory,
        "step": self.step_count
    }
    with open(f"/tmp/agent_checkpoint_{self.memory.get_working('trace_id')}.pkl", "wb") as f:
        pickle.dump(state, f)

4.4 工具沙箱安全

执行Shell命令和读写文件存在安全风险。工程化方案:

  • 限制工作目录(chroot或Docker容器)。
  • 使用白名单命令(禁用rm -rf /等)。
  • 对用户输入进行消毒。

代码语言:javascript
复制
# 在execute_shell中增加安全检查
ALLOWED_COMMANDS = {"ls", "cat", "grep", "find", "python", "pip"}
def execute_shell_safe(command: str, cwd: str = "."):
    cmd_parts = command.split()
    if cmd_parts and cmd_parts[0] not in ALLOWED_COMMANDS:
        raise ValueError(f"Command {cmd_parts[0]} not allowed")
    # 还可以检查路径是否在cwd内
    return execute_shell(command, cwd)

五、性能优化:并行工具调用

当规划器一次提出多个独立工具调用时,我们可以并行执行以大幅减少延迟。改造planner.step,允许action为数组。

代码语言:javascript
复制
# 修改planner返回结构:{"actions": [{"name": "read_file", "input": {...}}, ...]}
# 在orchestrator中并发执行
async def _execute_parallel(self, actions):
    tasks = []
    for act in actions:
        tool = self.tools.get_tool(act["name"])
        if tool:
            tasks.append(tool.execute(**act["input"]))
        else:
            tasks.append(asyncio.sleep(0, result=f"Error: tool {act['name']} not found"))
    results = await asyncio.gather(*tasks, return_exceptions=True)
    return results

但要注意,并发执行可能产生副作用(如同时写同一文件),需要加锁或文件锁。


六、完整案例:自动修复Python lint错误

让Agent执行一个实际任务:扫描项目中的.py文件,运行flake8,根据错误报告自动修复(导入缺失、空格、过长行等)。

我们只需增加一个run_flake8工具,然后让Planner按顺序执行:run_flake8read_filewrite_file → 重新run_flake8验证。

代码语言:javascript
复制
def run_flake8(path: str = ".") -> str:
    result = subprocess.run(
        ["flake8", path, "--format=json"],
        capture_output=True,
        text=True
    )
    if result.stdout:
        import json
        data = json.loads(result.stdout)
        # 格式化输出
        lines = []
        for filepath, errors in data.items():
            for err in errors:
                lines.append(f"{filepath}:{err['line']}:{err['column']} {err['code']} {err['text']}")
        return "\n".join(lines[:30])
    return "No flake8 errors found."

# 注册工具
registry.register(Tool("run_flake8", "Run flake8 and return errors", run_flake8))

然后运行Agent,它会自主规划:运行flake8→读取有错误的文件→修改→再验证。


七、部署与持续运营

7.1 容器化

使用Docker封装所有依赖,暴露HTTP API(FastAPI)接收任务请求。

代码语言:javascript
复制
# api.py
from fastapi import FastAPI, HTTPException
from pydantic import BaseModel
import asyncio

app = FastAPI()
class TaskRequest(BaseModel):
    query: str
    trace_id: Optional[str] = None

@app.post("/run")
async def run_task(req: TaskRequest):
    # 每个请求新建Orchestrator实例(或从池中获取)
    orchestrator = create_orchestrator()
    result = await orchestrator.run(req.query)
    return {"result": result, "trace_id": req.trace_id}

7.2 健康检查与熔断

  • 检查LLM API连通性。
  • 工具执行超时自动熔断(使用circuit-breaker模式)。

7.3 评估与迭代

  • 记录每次运行的所有步骤(轨迹),用于离线评估。
  • 使用BLEU、成功率等指标,用TruLens等框架进行自动化评估。

八、总结与展望

本文从零构建了一个具备工程化特质的AI编程助手Agent,覆盖了工具注册、记忆管理、规划循环、可观测性、安全沙箱、并发优化等关键环节。全部代码可复制运行(需配置OpenAI API Key),并可直接扩展为生产服务。

工程化的核心心法

  1. 将不确定性封装在边界内(工具执行、LLM调用)——外部系统保持确定性。
  2. 每个步骤都可记录、可回放——结构化日志是调试的命脉。
  3. 失败是常态,优雅降级是必备——重试、退避、备选路径缺一不可。

未来,Agent工程化将进一步走向自修复(Agent自动调整自己的prompt)、自优化(根据历史数据调整规划策略)和多Agent协作。但无论技术如何演进,扎实的工程底座永远是稳定输出的基石。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

目录
  • Agent工程化实战:从零构建可扩展的AI编程助手
    • 一、为什么需要“工程化”Agent?
    • 二、整体架构设计
    • 三、核心模块代码实现
      • 3.1 工具系统(Tool Registry)
      • 3.2 记忆管理器(Memory Manager)
      • 3.3 规划引擎(Planner)
      • 3.4 编排器(Orchestrator)
      • 3.5 具体工具实现(编程助手需要的能力)
      • 3.6 集成与启动入口
    • 四、工程化进阶:可观测性与容错
      • 4.1 结构化日志与追踪
      • 4.2 限流与并发控制
      • 4.3 状态持久化与恢复
      • 4.4 工具沙箱安全
    • 五、性能优化:并行工具调用
    • 六、完整案例:自动修复Python lint错误
    • 七、部署与持续运营
      • 7.1 容器化
      • 7.2 健康检查与熔断
      • 7.3 评估与迭代
    • 八、总结与展望
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档