尧图建网站 尧图建网站 YAOTU WEB BUILD 免费咨询
ARTICLE DETAIL

资讯详情

深耕网站建设与建站编程的一线实战洞察。

从Demo到生产:手写最小Agent服务,破解AI Agent不稳定与不可控难题

从Demo到生产:手写最小Agent服务,破解AI Agent不稳定与不可控难题 当时看 Manus 的演示很多人心里都会冒出一个念头这才是我想要的 AI。它不再只是聊天框里一句一句地挤答案而是像一位远程实习生一样自己打开网页、读取文件、整理信息最后交给你一份完整结果。这种体验确实让 Agent 第一次在大众层面变得可感知。但热闹过去之后真正在做工程的人会发现问题并没有消失反而更清楚了演示环境里能跑通的 Agent到了真实业务里为什么动不动就断任务一复杂为什么就开始乱调用工具角色一多为什么状态就失控于是就有了这篇文章想聊的话题——林俊旸和 Manus双双回到原点。这里的“原点”不是退步而是回到 Agent 工程最朴素的地基任务规划、工具权限、状态管理、结果验证、成本控制和可观测性。这篇文章不讨论人物八卦也不为任何产品站台。我会从工程视角拆解一个问题一个 Agent 从“拿得出手的 Demo”到“能上生产的服务”到底要补哪些课并且我们不用任何复杂的框架就用 Python FastAPI OpenAI 兼容接口从零搭一个最小但结构完整的 Agent 服务把整个循环跑通。如果你正在做 AI Agent、正在被“效果不稳定”和“流程不可控”折磨这篇文章应该能给你一个相对清晰的坐标系。1. 回到哪个“原点”从 Demo 到工程1.1 Agent 演示为什么让人觉得震撼传统聊天机器人给人的感觉是“有问有答”但 Agent 给人的感觉是“能办事”。为什么会有这种差异核心在于 Agent 引入了工具调用和任务循环。一个聊天机器人只做一件事根据用户输入生成文字。而一个 Agent 可以根据任务目标拆解步骤选择合适工具读取工具返回结果根据新信息继续决策直到任务完成或主动请求人工介入。Manus 当初最打动人的点就是它把这条链路以相对流畅的交互方式展示了出来。用户不需要理解背后的规划逻辑只需要看到它一步步把任务做完。这种“把复杂度藏起来”的产品体验是 Agent 概念破圈的真正原因。1.2 Demo 与可用之间的差距到底在哪如果说 Demo 是“在受控环境里证明路径可行”那生产系统就是“在不受控环境里保证结果可用”。两者之间差着好几层工程问题。第一层是稳定性。演示时任务通常是精心挑选的可能在固定网站、固定格式、固定网络环境下运行。真实业务里输入千奇百怪工具返回时好时坏模型偶尔抽风任何一个环节波动都可能导致 Agent 中断。第二层是可控性。演示时你可以接受 Agent 自由发挥但生产环境必须知道它下一步要做什么、为什么这样做、如果做错了怎么回滚。这要求 Agent 的每一步决策都有迹可循。第三层是成本。一次复杂任务可能需要调用几十次模型接口如果任务量大成本会指数级上升。演示时不需要关心 token 钱生产环境每一分钱都是成本。第四层是安全。给 Agent 开放工具时它可能访问到不该访问的数据也可能执行不该执行的命令。演示环境无所谓生产环境这就是事故。所以你会发现Manus 和很多 Agent 产品带来的最大价值不是“实现了 AGI”而是把 Agent 这个概念从论文推进到了产品验证阶段。而当行业开始认真做产品时所有人不约而同回到了工程原点。1.3 我的判断从开发者的视角看Agent 的下半场拼的不是谁的演示更炫而是谁能在工程层面把不确定性控制住。这篇文章就围绕这个判断展开。我会用最朴素的工程手段实现一个 Agent 服务不用 LangChain、不用 CrewAI甚至连异步框架都不上。因为很多 Agent 框架把复杂度遮蔽掉了你很难理解一个任务到底是怎么被执行的。当你亲手把循环写出来很多“为什么效果不稳定”的问题会立刻有答案。2. 再谈 Agent 核心概念别把 Demo 当产品2.1 Agent 到底是什么先给一个简单但不失准确的定义Agent 是一个能感知环境、做出决策、执行动作并基于执行结果继续调整的 AI 系统。这里的关键词是“循环”。传统 AI 应用是一次性输入输出Agent 则是多次“输入-决策-动作-观察-再决策”的循环。一个最基本的 Agent 至少包含五个部分大语言模型负责推理和决策任务目标用户当前想要完成的事情工具集Agent 可以调用的外部能力状态管理记录历史信息、中间结果和当前进度执行验证确认结果是否满足要求决定继续还是结束。缺少任何一部分Agent 都会变成“看起来很聪明但无法闭环”的玩具。2.2 Agent、工作流、RAG 的区别很多读者容易把 Agent、工作流和 RAG 混在一起这里用一个表格做区分。概念核心特征适合场景典型问题工作流固定流程步骤预先编排流程稳定、输入变化小无法处理未知分支RAG检索增强生成先查后答知识库问答、文档辅助只读不能执行动作Agent动态规划工具调用循环决策多步骤、需要外部动作稳定性、成本、安全简单来说工作流是“唱本子”RAG 是“查资料”Agent 是“边想边做”。三者并不互斥实际项目里经常组合使用先用 RAG 检索资料再用 Agent 决定调用哪些工具最后用固定工作流兜底。2.3 关键机制工具调用与 ReAct 循环在 OpenAI 的工具调用Function Calling出现之前开发者要让模型使用工具通常靠提示词约定 JSON 输出格式非常脆弱。Function Calling 的意义在于模型在生成时会被要求从给定工具列表中选择一个并输出结构化参数由程序解析后执行。而 ReAct 循环是一个经典的 Agent 思维框架模型先推理Reason当前状态然后决定执行什么动作Act观察结果后再推理下一步。名字虽然看起来高大上本质就是“想一下、做一下、看一眼结果、再想”。我建议每个 Agent 开发者都亲手实现一次 ReAct 循环。因为只有当你自己解析 tool_calls 时才会理解为什么 Agent 会“卡住”。2.4 为什么“回到原点”是回归工程回到原点的意思不是放弃语言模型而是认识到模型只是 Agent 的一个零件。真正决定一个 Agent 能不能用的是围绕模型搭建的工程系统。模型决定“聪明程度”工程决定“可靠程度”。大多数失败的 Agent 项目不是模型不够聪明而是工程结构不够稳。工具没有权限控制、状态没有持久化、失败没有重试、结果没有校验最后看起来就像“一个聪明人做着不靠谱的事”。3. 环境准备与最小场景设计3.1 场景技术报告分析助手为了不过度抽象我们设计一个真实感比较强的场景一个“技术报告分析助手”用户提交报告文件路径和统计请求Agent 读取本地文件、提取关键指标、做必要计算最后给出结论。这个场景具备 Agent 的典型要素需要读取工具需要计算工具需要根据文件内容决定下一步需要生成最终结论。同时它又足够安全不涉及网络请求也不会触发危险命令适合作为入门示例。3.2 环境准备示例使用 Python 3.10 以上版本建议先创建虚拟环境。mkdir agent-origin-demo cd agent-origin-demo python3 -m venv venv source venv/bin/activate pip install -U pip本文不指定第三方库的具体版本以实际安装时为准。核心依赖如下fastapi uvicorn openai pydantic其中 openai 库用于调用 LLM 服务的 OpenAI 兼容接口。如果你使用的是本地 Ollama只需要把 base_url 指向本机服务即可代码不用改。3.3 目录结构推荐按模块拆分不要让 Agent 逻辑和 API 层耦合在一个文件里。agent-origin-demo/ ├── requirements.txt ├── tools.py # 工具函数定义与实现 ├── llm_client.py # LLM 客户端封装 ├── agent.py # Agent 核心循环 ├── api.py # FastAPI 服务入口 ├── workspace/ # 工作目录存放报告文件 │ └── report.md └── run.py # 本地直接运行的入口这样的结构足够简单也能看出分层思想。如果以后要加数据库、任务队列可以继续往里扩展。4. 核心流程拆解4.1 任务拆解从用户 Query 到步骤用户提交一个自然语言请求后Agent 第一件事不是执行而是理解任务。在 ReAct 循环里这一步通常由模型在第一次决策时完成。例如用户说“读取 reports/demo.md 并汇总其中性能指标”模型需要意识到先调用 read_report 工具读取文件观察返回内容判断是否有指标数据如果有调用 aggregate_metrics 完成计算生成最终结论。这个拆解过程不需要预先编写而是模型基于工具描述和用户目标动态生成。这就是 Agent 与固定工作流的本质区别。4.2 工具设计给 Agent 最小可用能力工具是 Agent 的“手”设计工具时必须克制。一个常见错误是给 Agent 塞了太多工具导致它频繁选错。工具描述非常关键。模型只能通过 description 理解工具用途所以描述要清晰、具体最好包含使用场景示例。另一个关键点是工具参数需要有约束。比如 path 参数必须给定values 参数必须是数字数组不能放任模型自由发挥。4.3 执行循环决策—调用—观察Agent 的核心循环可以浓缩成一句话把模型当成“指挥官”把工具当成“士兵”每轮先让指挥官给指令执行完士兵汇报结果指挥官再决定下一步。这个循环有几个容易踩的坑没有设置最大步数模型陷入死循环工具调用后没有把结果传回模型模型无法继续解析 tool_calls 时崩溃导致整个任务失败模型返回空内容但 Agent 仍然继续。后面示例代码会逐一处理这些问题。4.4 结果确认与人工介入在演示环境里Agent 执行完就可以返回结果。但生产环境中越关键的操作越需要人工确认。一个合理的策略是对于只读工具Agent 可以自动执行对于写操作、删除操作、支付操作必须设置审批节点。这个策略在后面的最佳实践部分会详细展开。在最小示例中我们先不做人工审批但会在代码结构上预留判断入口。5. 完整示例代码实现5.1 工具实现文件路径tools.py# -*- coding: utf-8 -*- 工具函数定义与实现。 每个工具都尽量保持单一职责避免一个函数做太多事情。 import json from pathlib import Path BASE_DIR Path.cwd() / workspace def read_report(path: str) - dict: 读取 workspace 目录下的报告文件。 为了安全限制只能读取 workspace 内的文件防止路径穿越。 try: target (BASE_DIR / path).resolve() if not str(target).startswith(str(BASE_DIR.resolve())): return {error: 路径越界拒绝访问} if not target.exists(): return {error: f文件不存在: {path}} content target.read_text(encodingutf-8) return {path: str(target), content: content[:2000]} except Exception as exc: return {error: f读取文件失败: {exc}} def aggregate_metrics(values: list) - dict: 计算一组数值指标的汇总信息。 if not values: return {sum: 0, count: 0, avg: 0} total sum(values) count len(values) return { sum: total, count: count, avg: round(total / count, 2), } TOOL_DEFINITIONS [ { type: function, function: { name: read_report, description: 读取工作目录中的报告文件返回文件内容。适用于需要分析报告文本的场景。, parameters: { type: object, properties: { path: { type: string, description: 报告文件的相对路径例如 report.md } }, required: [path] } } }, { type: function, function: { name: aggregate_metrics, description: 计算一组数值指标的总和、数量与平均值。适用于需要对报告中的指标做统计的场景。, parameters: { type: object, properties: { values: { type: array, items: {type: number}, description: 数值列表 } }, required: [values] } } } ] def dispatch_tool(name: str, arguments: dict) - dict: 工具分发器根据工具名称调用对应函数。 不要直接把用户输入拼进工具名防止注入。 if name read_report: return read_report(arguments.get(path, )) if name aggregate_metrics: values arguments.get(values, []) return aggregate_metrics(values) return {error: f未知工具: {name}}这里特别说明一下路径安全。Agent 调用工具时参数来自模型而模型可能被用户输入引导。所以工具内部必须做路径校验。示例使用的是一种简单的白名单校验能挡住../路径穿越但不等于绝对安全生产环境建议把工作目录放到独立沙箱里。5.2 LLM 客户端封装文件路径llm_client.py# -*- coding: utf-8 -*- LLM 客户端封装统一管理模型调用。 支持 OpenAI 官方接口也支持任何兼容 OpenAI 协议的本地服务。 import os from openai import OpenAI class LLMClient: def __init__(self): self.client OpenAI( base_urlos.getenv(LLM_BASE_URL), api_keyos.getenv(LLM_API_KEY, not-needed), ) # 这里的默认模型名只是一个占位示例请根据实际服务修改 self.model os.getenv(LLM_MODEL, gpt-4o-mini) def chat(self, messages: list, tools: list None): 调用模型对话接口。 tools 为空时不传避免部分服务不支持。 kwargs { model: self.model, messages: messages, } if tools: kwargs[tools] tools response self.client.chat.completions.create(**kwargs) return response.choices[0].message使用环境变量可以避免把密钥写进代码。如果你的模型服务不需要密钥例如本地 Ollama可以设置LLM_API_KEYnot-needed并将LLM_BASE_URL指向本地地址。5.3 Agent 核心循环文件路径agent.py# -*- coding: utf-8 -*- 最小 ReAct Agent 循环。 核心思路模型决策 - 程序执行工具 - 返回结果 - 模型继续决策 import json from llm_client import LLMClient from tools import TOOL_DEFINITIONS, dispatch_tool MAX_STEPS 5 SYSTEM_PROMPT 你是一个技术报告分析助手。 你需要根据用户的问题逐步调取工具获取信息并最终给出分析结论。 每一步都要清楚你当前需要什么信息调用哪个工具然后根据结果继续。 不要臆造不存在的文件或指标所有结论必须基于工具返回的真实内容。 def run_agent(user_query: str, max_steps: int MAX_STEPS) - str: llm LLMClient() messages [ {role: system, content: SYSTEM_PROMPT}, {role: user, content: user_query}, ] for step in range(max_steps): # 1. 让模型决策 assistant_message llm.chat(messages, toolsTOOL_DEFINITIONS) # 2. 记录模型回复 assistant_payload { role: assistant, content: assistant_message.content or , } # 3. 如果模型要求调用工具 if assistant_message.tool_calls: tool_calls [] for tc in assistant_message.tool_calls: tool_calls.append({ id: tc.id, type: function, function: { name: tc.function.name, arguments: tc.function.arguments, }, }) assistant_payload[tool_calls] tool_calls messages.append(assistant_payload) # 4. 没有工具调用说明模型认为可以结束了 if not assistant_message.tool_calls: return assistant_message.content or 模型未返回内容 # 5. 逐个执行工具并把结果返回给模型 for tc in assistant_message.tool_calls: name tc.function.name try: arguments json.loads(tc.function.arguments or {}) except json.JSONDecodeError: arguments {} result dispatch_tool(name, arguments) messages.append({ role: tool, tool_call_id: tc.id, content: json.dumps(result, ensure_asciiFalse), }) return 已达最大步数任务未能完成。请简化问题或检查工具配置。这个循环看起来简单但已经覆盖了 Agent 最核心的执行链路。你没有依赖任何 Agent 框架所以每一步都清清楚楚。5.4 FastAPI 服务入口文件路径api.py# -*- coding: utf-8 -*- FastAPI 服务入口把 Agent 能力包装成 HTTP 接口。 from fastapi import FastAPI, HTTPException from pydantic import BaseModel from agent import run_agent app FastAPI(titleMinimal Agent Demo) class AgentRequest(BaseModel): query: str class AgentResponse(BaseModel): ok: bool result: str app.post(/agent/run, response_modelAgentResponse) def agent_run(req: AgentRequest): if not req.query.strip(): raise HTTPException(status_code400, detailquery 不能为空) try: result run_agent(req.query) except Exception as exc: # 这里只做最小异常处理生产环境应该记录完整 traceback raise HTTPException(status_code500, detailfAgent 执行失败: {exc}) return AgentResponse(okTrue, resultresult)5.5 本地直接运行入口文件路径run.py# -*- coding: utf-8 -*- 本地直接运行入口方便在没有启动 HTTP 服务时快速调试。 from agent import run_agent if __name__ __main__: query 读取 report.md 并汇总其中所有性能指标 result run_agent(query) print(result)6. 运行结果与效果验证6.1 准备测试文件在 workspace 目录下创建report.md# 2025 年第一季度系统性能报告 - 接口平均响应时间: 120ms - 接口最大响应时间: 350ms - 每日请求总量: 860000 - 错误率: 0.02% ## 结论 系统整体运行稳定响应时间符合 SLO 目标。这个文件内容很简单但足够让 Agent 执行一次“读取 提取 汇总”的完整链路。6.2 启动服务先配置环境变量。export LLM_BASE_URLhttps://api.openai.com/v1 export LLM_API_KEY你的密钥 export LLM_MODELgpt-4o-mini如果你使用 Ollama大致是这样export LLM_BASE_URLhttp://localhost:11434/v1 export LLM_API_KEYnot-needed export LLM_MODELqwen2.5:7b然后启动服务uvicorn api:app --host 0.0.0.0 --port 8000看到类似下面的日志说明启动成功INFO: Uvicorn running on http://0.0.0.0:80006.3 发送测试请求用 curl 调用接口curl -X POST http://127.0.0.1:8000/agent/run \ -H Content-Type: application/json \ -d {query: 读取 report.md 并汇总其中所有性能指标}如果一切正常返回结果大致是这样的结构{ ok: true, result: 根据报告内容我提取到以下性能指标平均响应时间 120ms最大响应时间 350ms每日请求量 860000错误率 0.02%。对平均响应时间、最大响应时间和每日请求量进行汇总三个指标合计为 860470平均为 286823.33。错误率属于比例指标不参与算术汇总。整体来看系统运行稳定。 }注意真实结果会因模型能力不同而略有差异但结构应该一致。6.4 如何判断 Agent 是否成功判断标准不只是“有没有返回文字”而是是否读取了文件是否提取了正确的指标是否调用了 aggregate_metrics最终结论是否基于工具返回的真实数据是否没有臆造文件内容。如果你的 Agent 没有调用工具而是直接编造了一份报告那说明提示词或工具描述需要调整。这是 Agent 调试中最常见的问题。7. 常见问题与排查思路以下是 Agent 开发中最常见的问题和排查方法。问题现象可能原因排查方式解决方案模型不调用工具直接生成答案工具描述不清晰或模型能力不足打印 messages 中模型返回内容优化工具 description换用支持 Function Calling 的模型工具调用参数解析失败模型返回的 arguments 不是合法 JSON捕获 JSONDecodeError 并打印原始内容在解析失败时把错误信息返回模型让它重新生成Agent 陷入死循环没有设置最大步数或工具结果无法帮助模型决策检查每一步 tool result 是否包含关键信息设置 max_steps并在结果里增加“下一步建议”返回内容与文件实际内容不一致上下文被截断或模型过度推理检查传给模型的文件内容是否完整扩大内容截断长度或增加“只基于事实回答”指令路径权限被拒绝路径校验规则过于严格查看错误信息中的路径对比日志调整工作目录白名单调用本地模型报错模型服务不支持 Function Calling查看模型服务日志换用支持工具调用的模型例如 qwen2.5 系列接口响应超时Agent 循环耗时太长为 HTTP 请求配置超时时间使用异步任务 轮询或 WebSocket 返回进度这里最容易被忽视的是第一条。很多时候不是代码有问题而是模型根本不理解工具是干什么的。工具描述必须写得像“给一个新同事的说明书”而不是一行干巴巴的注释。8. 从 Demo 到生产的工程建议8.1 评测先行Agent 最怕“感觉好用但说不出哪里好”。一定要为每个任务准备评测集至少包含正常输入边界输入工具失败输入恶意输入。每次改动 Prompt、工具或模型后都跑一遍评测集记录成功率和失败原因。没有评测的 Agent 优化基本等于碰运气。8.2 工具权限最小化给 Agent 的工具列表越短越好。只开放完成当前任务必要的能力不要一上来就给它 Shell、数据库删除权限、支付接口。工具内部要做二次校验。就像示例里的文件路径白名单一样看起来多写几行代码但能挡住大多数误操作。8.3 状态持久化与恢复Demo 里 Agent 跑完就结束了但生产环境任务可能持续几分钟甚至几小时。你需要把任务状态持久化到数据库包括当前步骤已产生的中间结果已经调用过的工具剩余步骤数。否则服务一重启任务就丢了。这个模块通常被称为“任务状态表”是 Agent 工程化和玩具 Demo 的分水岭。8.4 可观测性生产环境的 Agent 必须可观测。建议把每一步的关键信息输出到日志当前任务 ID模型输入输出 token 数工具名称与参数工具返回结果耗时错误信息。有了这些日志你才能回答“它刚才是怎么想的”“它为什么调用了这个工具”。8.5 成本控制Agent 的 token 消耗会比普通对话高出一个量级。每一轮工具调用都会把工具结果和历史消息重新发送给模型。优化思路包括控制历史消息长度只保留与当前任务相关的片段工具结果设置截断使用模型缓存功能设置单任务成本上限。在示例代码里我已经对文件内容做了截断这就是一种非常朴素的成本控制。8.6 人工审批节点不是所有步骤都适合让 Agent 自动完成。建议把工具分为“自动执行”和“需审批执行”两类。例如读取文件自动执行计算自动发送消息审批修改数据审批删除资源禁止。审批节点会让 Agent 看起来没那么“自动”但换来了安全边界。在真实业务里安全永远优先于体验。8.7 输出校验与兜底模型输出的内容不一定符合要求。生产级 Agent 应该在返回最终结果前增加一个校验环节例如是否包含必填字段是否引用了不存在的文件是否与工具返回数据矛盾。校验不通过时可以重新让模型生成也可以直接返回“需要人工处理”。不要总指望模型一次做对。8.8 团队协作与代码组织Agent 项目的代码组织不应该因为“它是 AI 项目”就特殊化。工具模块、提示词模块、状态管理模块、API 层都要像普通后端服务一样分层管理。提示词建议单独抽成文件或配置中心而不是散落在代码里。这样产品同学可以调开发同学可以追踪版本模型升级时也能快速对比效果。9. 总结与下一步实践回到标题说的“回到原点”。Manus 和林俊旸这两个名字被放在一起讨论时人们看到的往往是光环、争议和情绪。但作为开发者我更愿意把这个话题理解成一次提醒Agent 的价值不在于演示时的那几分钟而在于它能否在真实业务场景里稳定、安全、低成本地完成任务。这篇文章里我们用不到两百行代码实现了一个最小 Agent 服务。它没有复杂框架也没有分布式架构但它把 Agent 最核心的执行链路完整跑通了模型决策、工具调用、结果回传、循环终止。我的建议是你先不要急着给项目引入重型框架而是把这段代码在本地跑通仔细观察模型每一步的返回尤其是 tool_calls 的结构。当你亲手处理过一次 JSON 解析错误、看过一次模型不调用工具的死循环你对 Agent 的理解会立刻不一样。下一步可以往这几个方向深入给 Agent 增加持久化存储让它支持长时间任务引入人工审批节点让写操作先经过管理员确认建立评测集和回归测试每次修改之后都用数据说话尝试接入不同的本地模型对比它们在工具调用上的表现差异。Agent 的原点从来不是某一个模型或某一个产品而是工程。谁能把不确定性控制住谁就能把 Demo 变成真正的生产力。
返回列表