news 2026/10/8 2:04:48

拆解 Agent 核心原理|从零动手实现简易 AI 智能体(六)

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
拆解 Agent 核心原理|从零动手实现简易 AI 智能体(六)

本文摘要:智能体此前只能在命令行里交互,外部程序拿不到回复,能力被锁在本地进程。新增的 api.py 用 FastAPI 把它包成 HTTP 接口,请求体自动校验、回复包成 JSON 返回。

一、环境与前提

上一篇(第五篇)完成多轮对话的状态管理与记忆维护,本篇解决把agent-demo/里的Agent与run_agent暴露成 HTTP API 的问题。

调用约定沿用前五篇:端点内调用run_agent,传入一条用户输入字符串,拿到回复字符串,main.py一行不改。若你的run_agent需要显式接收Agent实例,在调用处多传一个参数即可,写法见第四节末尾。

前置条件(环境配置与_truncate_messages的截断策略见前几篇,此处不重复展开):Python 3.11+、openai 1.x、tiktoken、python-dotenv已安装;agent-demo/目录下main.py提供Agent类(__init__、_count_tokens、_truncate_messages)、run_agent、image_to_base64、main;.env中已配置OPENAI_API_KEY。

本篇只新增api.py,不动main.py:命令行入口继续可用,服务出问题时用python main.py对照一次,就能区分是接口层的问题还是Agent本身的问题。

本篇新增两个依赖,具体版本以 pip 实际安装到的为准(未确认版本):

依赖在本篇的作用
fastapi定义路由、用 Pydantic 校验请求体、用Depends做依赖注入
uvicorn运行 FastAPI 生成的 ASGI 应用

步骤 1:安装依赖并确认导入链完好

目的:装上 Web 框架与 ASGI 服务器,同时确认main.py的既有名称仍可导入,不被新增依赖影响。

操作:在agent-demo/目录下依次执行:

pipinstallfastapi uvicorn python-c"import fastapi, uvicorn; print('ok')"python-c"from main import Agent, run_agent, image_to_base64, main; print('ok')"

预期输出:后两条命令各回显一行:

ok

实际输出:未实测;两条命令成功时都只回显ok,缺包时抛ModuleNotFoundError,名称不齐时抛ImportError。

若导入main的那条卡住不返回,说明main.py顶层直接调用了main(),给它补上if __name__ == "__main__":守卫即可(未实测)。

二、关键步骤

一次请求的完整链路是:curl把 JSON 发到/chat→uvicorn把请求交给 FastAPI → FastAPI 用ChatRequest校验并构造请求对象 →chat函数调用run_agent→ 返回值被ChatResponse序列化成 JSON。下面按这条链路落地。

步骤 2:新建agent-demo/api.py

目的:用 Pydantic 定义请求与响应模型,暴露一个POST /chat端点去调用run_agent,main.py保持不动。

操作:新建文件agent-demo/api.py,写入以下完整内容:

fromfastapiimportFastAPIfrompydanticimportBaseModelfrommainimportrun_agentclassChatRequest(BaseModel):message:strclassChatResponse(BaseModel):reply:strapp=FastAPI()@app.post("/chat",response_model=ChatResponse)defchat(request:ChatRequest)->ChatResponse:reply=run_agent(request.message)returnChatResponse(reply=reply)

四点说明:

  • 路由函数chat用同步def声明,run_agent同样是同步函数,FastAPI 会把它放进线程池执行,不会阻塞事件循环(依据:FastAPI 官方文档 Defining Asynchronous and Synchronous Functions)。
  • ChatRequest的message: str让 FastAPI 自动生成请求体校验,字段缺失或类型不符会直接返回校验错误,不用手写判断。
  • response_model=ChatResponse决定响应体只含reply字段,同时把该结构写进 OpenAPI 文档,/docs里能看到请求与响应的字段。
  • app = FastAPI()这一行的实例名必须是app,uvicorn api:app靠它定位应用对象,写成别的名字会启动失败(见第三节)。

这个文件不构造Agent实例、也不碰self.messages,api.py只做协议转换,状态问题留给第三节。

操作(校验导入):仍在agent-demo/目录下执行:

python-c"import api; print(type(api.app).__name__)"

预期输出:

FastAPI

实际输出:未实测;成功时仅回显FastAPI,main.py导入失败时会先抛出它的原始异常。

步骤 3:启动服务

目的:把应用跑在本地端口上,供下一步调用。

操作:在agent-demo/目录下执行(--reload会在文件改动后自动重启,仅用于开发;按Ctrl+C停止):

uvicorn api:app--reload

预期输出(启动行的具体格式随版本略有差异,未确认版本):

INFO: Uvicorn running on http://127.0.0.1:8000 (Press CTRL+C to quit) INFO: Application startup complete.

实际输出:未实测;进程保持前台运行,控制台出现服务地址与Application startup complete.。

启动成功后,浏览器打开[/docs](/docs)能看到 FastAPI 自动生成的交互文档,/chat端点应出现在列表里。默认只绑定127.0.0.1,仅本机可访问;要让局域网内其他机器调用,启动命令需加--host 0.0.0.0(未实测,按 uvicorn 参数语义)。

步骤 4:发送一次正常请求

目的:确认端点真的把用户输入交给run_agent,并把回复包成 JSON 返回。

操作:另开一个终端(服务保持运行),执行:

curl-XPOST http://127.0.0.1:8000/chat\-H"Content-Type: application/json"\-d'{"message": "用一句话介绍你自己"}'

Content-Type: application/json不能省:FastAPI 按 JSON 解析请求体,缺这个请求头会直接返回校验错误。

预期输出:

{"reply":"<Agent 的回复文本>"}

实际输出:未实测;响应体是含reply字段的 JSON 对象,reply的文本由模型生成,每次调用内容不同。

步骤 5:验证请求体校验

目的:确认字段写错时服务端直接拒绝,校验不用自己写。

操作:

curl-i-XPOST http://127.0.0.1:8000/chat\-H"Content-Type: application/json"\-d'{"msg": "字段名写错了"}'

预期输出:状态行为HTTP/1.1 422 Unprocessable Entity,响应体形如:

{"detail":[{"loc":["body","message"],"msg":"Field required","type":"missing"}]}

实际输出:未实测;返回校验失败响应,detail数组元素包含loc、msg、type字段(形态依据 FastAPI 官方文档)。

三、失败处理

失败 1:uvicorn 找不到应用对象

报错原文:

AttributeError: module 'api' has no attribute 'app'

模块名写错时报错原文:

Error loading ASGI app. Could not import module "api".

原因:uvicorn 模块名:应用对象名要求模块里确实存在该名称的 ASGI 应用。常见触发方式有两种:FastAPI 实例没有命名为app(例如写成application = FastAPI());或者在agent-demo/目录下写成uvicorn main:app,模块名与app所在的文件不一致。

修复:把实例名改回app,启动命令的模块名与文件名对齐。

操作:在agent-demo/目录下执行:

uvicorn api:app--reload

预期输出:与步骤 3 相同的启动行。

实际输出:未实测;修复后启动行与步骤 3 一致。

失败 2:共享Agent实例导致对话状态串扰

症状:并发请求时,一个请求写入的对话历史出现在另一个请求的上下文里;历史累积到超限时_truncate_messages(第五篇)会静默截断,用户侧表现为 Agent「忘记」早期对话。整个过程没有异常抛出。

原因:第五篇在Agent里用self.messages维护对话历史。若为了让 API「记住上下文」,把Agent实例化成模块级变量并让所有请求共用,历史就会跨请求累积。Depends本身每次请求都会调用依赖函数,但函数返回的是同一个模块级对象,注入的仍是同一实例(依据:FastAPI 官方文档 Dependencies)。

复现:新建文件agent-demo/api_shared.py,写入以下完整内容:

fromfastapiimportDepends,FastAPIfrompydanticimportBaseModelfrommainimportAgent,run_agent app=FastAPI()shared_agent=Agent()defget_agent()->Agent:returnshared_agentclassChatRequest(BaseModel):message:strclassChatResponse(BaseModel):reply:str@app.post("/chat",response_model=ChatResponse)defchat(request:ChatRequest,agent:Agent=Depends(get_agent))->ChatResponse:reply=run_agent(request.message)agent.messages.append({"role":"user","content":request.message})agent.messages.append({"role":"assistant","content":reply})returnChatResponse(reply=reply)@app.get("/history")defhistory(agent:Agent=Depends(get_agent))->dict:return{"messages":agent.messages}

操作:用uvicorn api_shared:app --reload启动,另开终端连续发两次请求,再看历史接口:

curl-XPOST http://127.0.0.1:8000/chat\-H"Content-Type: application/json"\-d'{"message": "用户甲的第一条"}'curl-XPOST http://127.0.0.1:8000/chat\-H"Content-Type: application/json"\-d'{"message": "用户乙的第一条"}'curlhttp://127.0.0.1:8000/history

预期输出:/history返回的messages里混着两次请求写入的全部消息,用户甲的内容出现在用户乙的会话记录中。

实际输出:未实测;串扰直接体现在/history的messages中(若生成回复时读取同一实例的self.messages,他人对话还会进入回复正文,此处机制为推测)。

修复:让每个请求拿到独立实例,只改get_agent这一个函数:

defget_agent()->Agent:returnAgent()

操作:重启服务后再执行上一条curl [/history](/history)。

预期输出:messages只包含本次请求写入的消息。

实际输出:未实测;修复后/history只含本次请求写入的消息,不再出现其他请求的内容。

代价要说清:修复后跨请求不再有对话连续性;要维持会话,需要按会话标识隔离历史并引入额外存储,超出本篇范围。

失败 3:执行目录不对导致导入失败

报错原文(在agent-demo/之外执行python -c "import api"):

ModuleNotFoundError: No module named 'main'

原因:api.py用from main import run_agent导入同目录模块,Python 只在当前工作目录与sys.path中查找main;执行位置不在agent-demo/时就找不到main,uvicorn 侧则表现为找不到api模块。

修复:先切到agent-demo/再执行任何命令。

操作:

cdagent-demo python-c"import api; print('ok')"

预期输出:

ok

实际输出:未实测;成功时仅回显ok。

四、替代方案与取舍

两个决定点各有两种做法。先看Agent实例的管理方式:

维度方案 A:单例(模块级共享)方案 B:每请求新建(本篇默认)
适用条件本地开发、单用户调试多用户服务、面向外部程序调用
代价对话状态跨请求累积,不同用户互相污染每次请求重新初始化Agent,tiktoken编码等固定开销重复支付
边界并发一超过单用户就出现串扰,不能用于多用户没有跨请求对话连续性;要维持会话需额外存储与会话管理,超出本篇范围

单例只有在一个人调试时才成立,一旦有两个调用方,它的边界就到了。每请求新建把状态彻底隔离,代价是放弃跨请求记忆,这也是第二节api.py不持有任何Agent实例的原因。

再看路由处理函数的声明方式:

维度方案 A:同步def(本篇默认)方案 B:异步async def+asyncio.to_thread
适用条件run_agent是同步函数,直接调用最省事需要更高并发,愿意多写一层异步包装
代价并发能力受 FastAPI 默认线程池大小限制多一次线程调度,代码与调试都更复杂
边界低流量、内部工具够用在async def里直接调用同步run_agent会阻塞事件循环,延迟反而更高

方案 B 的完整实现如下(独立文件agent-demo/api_async.py):

importasynciofromfastapiimportFastAPIfrompydanticimportBaseModelfrommainimportrun_agentclassChatRequest(BaseModel):message:strclassChatResponse(BaseModel):reply:strapp=FastAPI()@app.post("/chat",response_model=ChatResponse)asyncdefchat(request:ChatRequest)->ChatResponse:reply=awaitasyncio.to_thread(run_agent,request.message)returnChatResponse(reply=reply)

操作:在agent-demo/目录下用uvicorn api_async:app --reload启动,再执行步骤 4 的那条curl。

预期输出:与步骤 4 相同的 JSON 结构。

实际输出:未实测;响应体结构与步骤 4 一致。

若你的run_agent需要显式接收Agent实例:把Depends(get_agent)(每请求新建)加进路由参数,同步版写成run_agent(agent, request.message),异步版写成await asyncio.to_thread(run_agent, agent, request.message)。

以下情况不该用本篇方案:

  • 需要跨请求的多轮记忆:/chat每次请求都是新实例,别用它做必须记住上文的产品,先补会话存储再谈接口。
  • 直接暴露公网:本篇没有鉴权与限流,不应作为公网服务运行。
  • 生产部署:--reload是开发期的自动重载,多进程部署与运维不在本篇范围。

下一步要解决的问题是:在这个 API 服务上增加流式输出,让用户逐步看到 Agent 的推理与回复过程。

参考资料

n8n-io/n8n
Significant-Gravitas/AutoGPT
huggingface/transformers

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/8 2:02:59

RDMA核心技术解析:WQ、QP、CQ工作原理与RoCEv2实战避坑

简介&#xff1a;本资源是一份系统性的RDMA技术调研报告&#xff0c;面向网络工程师、高性能计算开发者及云计算架构师等技术人员&#xff0c;聚焦低延迟通信场景下的核心加速技术原理与落地实践。内容涵盖RDMA基础概念、零拷贝/内核旁路/CPU卸载三大优势解析&#xff0c;Infin…

作者头像 李华
网站建设 2026/10/8 2:02:30

Chrome MCP Server MCP 服务说明文档

1. 服务概述一句话简介&#xff1a;通过MCP提供Chrome DevTools Protocol集成&#xff0c;允许您通过连接到Chrome的开发者工具来调试Web应用程序。服务名称&#xff1a;Chrome MCP Server版本号&#xff1a;最新版本开发者/提供方&#xff1a;benjaminr协议类型&#xff1a;MC…

作者头像 李华
网站建设 2026/10/8 2:00:17

从最小循环到可靠系统:AI Agent工程化实践指南

去年我花了两个晚上写出了人生第一个真正的Agent&#xff1a;模型拿到用户问题&#xff0c;自己决定调用天气接口&#xff0c;把结果包装成一段回答。跑通的那一刻真的很兴奋——AI Agent原来就是这么回事。但第三天冷静下来&#xff0c;我发现这个最小循环只在演示环境里成立。…

作者头像 李华