首页 / 资讯中心 / 文章详情

AI Agent 入门:从手写最小可行内核开始

AI Agent 入门:从手写最小可行内核开始 ★ FEATURED ARTICLE
1. 别急着装框架AI Agent 学习路径的“反常识”起点我带过三十多个从零转行进 AI 工程领域的学员其中超过七成在第一周就卡死在 LangChain 的AgentExecutor初始化报错里——不是代码写错了而是根本没搞清自己到底在让谁“执行”什么。他们打开教程第一行就是pip install langchain第二行from langchain.agents import initialize_agent第三行就开始调agent.run(帮我查下今天北京天气)……结果模型返回一串乱码本地 LLM 崩溃向量库连不上连日志都看不懂在哪看。这不是能力问题是学习路径被严重污染了。AI Agent 不是“装个包就能跑”的玩具它是一套有明确因果链的决策系统输入 → 理解意图 → 拆解任务 → 调用工具 → 整合结果 → 生成响应。这链条里任何一环断裂整个 Agent 就会像断线木偶一样僵住。而绝大多数新手一上来就试图把整条链塞进一个initialize_agent()函数里还指望它自动修复所有断点——这就像教人开车不先讲油门刹车离合怎么配合直接让人坐上 F1 赛车调校 ECU 参数。真正该先学的根本不是 LangChain、CrewAI 或 Dify 这些“方向盘和仪表盘”而是方向盘底下那套传动轴、差速器和发动机活塞运动原理。具体来说就是三个不可跳过的底层能力Python 的函数式思维与状态管理能力、LLM 的 token 级别行为直觉、以及工具调用Tool Calling的协议本质。这三个东西决定了你后续是能“调试 Agent”还是只能“重启 Agent”。提示所谓“AI Agent 入门”本质是学会如何把人类模糊的自然语言指令翻译成机器可验证、可回溯、可中断的确定性操作序列。这个翻译过程不依赖任何框架只依赖你对 Python 执行流、LLM 输出结构、HTTP/CLI 工具接口的理解深度。我见过太多人花两周时间配好 LangGraph 的 StateGraph却在调试一个search_web工具时花了三天才意识到不是模型没调用工具而是工具返回的 JSON 字段名和 Agent 解析器约定的字段名差了一个下划线。这种问题LangChain 文档不会告诉你但如果你亲手写过三次requests.get()json.loads()if result in data:的完整流程你一眼就能定位。所以别碰框架。先用纯 Python 写一个能跑通的“最小可行 Agent”它不联网、不向量检索、不画流程图只做一件事——接收用户一句话判断这句话是否需要调用计算器、天气 API 或维基百科搜索然后调用对应函数把结果原样返回。这个过程你会被迫搞懂input是字符串output是字典tool_call是函数名参数字典tool_result是函数返回值而final_answer是最终拼接的字符串。这些才是 Agent 的“原子操作”比任何.run()方法都真实。2. 从零手写第一个 Agent三步构建可调试的决策内核我们来动手写一个真正“看得见、摸得着、改得了”的 Agent 内核。它只有 87 行 Python 代码不依赖 LangChain不调外部模型用本地ollama的phi3:mini或 OpenAI 的gpt-3.5-turbo即可运行。重点不是功能多强而是让你看清每个环节的输入输出、数据流向和失败点。2.1 第一步定义清晰的工具契约Tool ContractAgent 的核心不是“思考”而是“调度”。调度的前提是每个工具必须有明确、无歧义、可序列化的契约。很多框架崩溃根源在于工具定义太随意。比如# ❌ 错误示范契约模糊无法被解析 def get_weather(city): return f{city}今天晴25度 # ✅ 正确示范契约显式含 name、description、parameters 三要素 weather_tool { name: get_weather, description: 获取指定城市的当前天气信息返回温度、天气状况和湿度, parameters: { type: object, properties: { city: {type: string, description: 城市名称如北京、上海} }, required: [city] } }这个weather_tool字典就是 Agent 和工具之间的“合同”。它告诉模型“如果用户问天气请调用get_weather并传入{city: xxx}”。模型输出的tool_calls必须严格匹配这个结构你的 Python 代码才能安全地getattr(tool_module, call[name])(**call[args])。我实测发现新手最容易忽略required字段。当模型返回{city: null}时get_weather(None)直接抛出TypeError。而加上required: [city]后模型会主动补全缺失字段或拒绝生成无效调用。这不是模型更聪明了是你给它画了清晰的边界。2.2 第二步实现工具调用与结果注入The Real Loop真正的 Agent 循环远比agent.run()复杂。它是一个带状态的 while 循环每轮处理一个tool_call直到模型返回final_answer。以下是精简但完整的逻辑import json import requests def run_agent(user_input: str, tools: list, llm_client): # Step 1: 初始 prompt包含工具描述 system_prompt f你是一个AI助手。可用工具如下 {json.dumps(tools, ensure_asciiFalse, indent2)} 请根据用户问题选择合适的工具调用。若无需工具直接回答。 messages [{role: system, content: system_prompt}, {role: user, content: user_input}] # Step 2: 主循环最多尝试5轮防死循环 for step in range(5): # 调用 LLM要求其输出 JSON 格式的 tool_calls 或 final_answer response llm_client.chat.completions.create( modelphi3:mini, messagesmessages, temperature0.1, response_format{type: json_object} # 强制 JSON 输出 ) content json.loads(response.choices[0].message.content) # 判断是 tool_call 还是 final_answer if tool_calls in content and content[tool_calls]: # 处理工具调用 tool_results [] for call in content[tool_calls]: try: # 安全执行通过工具名映射到函数 func globals().get(call[name]) if not func: raise ValueError(fUnknown tool: {call[name]}) result func(**call[args]) tool_results.append({ name: call[name], result: result }) except Exception as e: tool_results.append({ name: call[name], error: str(e) }) # 将工具结果注入消息历史供下一轮推理 messages.append({ role: assistant, content: json.dumps({tool_calls: content[tool_calls]}, ensure_asciiFalse) }) messages.append({ role: tool, content: json.dumps(tool_results, ensure_asciiFalse) }) elif final_answer in content: return content[final_answer] else: # 模型未按约定输出强制终止 return 模型输出格式错误请重试 return Agent 执行超时已终止这段代码的关键在于messages列表的动态构建。它不是静态的 prompt而是带记忆的对话状态机assistant消息记录调用意图tool消息注入实际结果。LLM 下一轮看到的是“我上次说要调 weather现在拿到了北京25度晴”而不是“用户问天气”。这种状态注入才是 Agent 能连贯思考的基础。框架封装了这个细节但你必须亲手写一次才能理解为什么state在 LangGraph 里那么重要。2.3 第三步亲手写一个真实工具Calculator别用现成的serpapi或wikipedia。先写一个计算器工具它足够简单却暴露所有关键问题import re def calculate(expression: str) - str: 安全计算数学表达式仅支持 - * / ( ) 和数字 防注入移除所有字母、分号、exec 等危险字符 # 白名单过滤只保留数字、小数点、四则运算符和括号 safe_expr re.sub(r[^0-9\-*/().\s], , expression) # 防止空表达式或纯符号 if not safe_expr or re.fullmatch(r[\-*/().\s]*, safe_expr): return 表达式无效请输入数字和运算符 try: # 使用 eval但已在白名单过滤后风险可控 result eval(safe_expr) return f计算结果{result} except ZeroDivisionError: return 错误除数不能为零 except SyntaxError: return 错误表达式语法不正确 except Exception as e: return f计算异常{str(e)}这个calculate函数教会你三件事工具输入必须清洗用户可能输入11; os.system(rm -rf /)白名单过滤是第一道防线错误必须结构化返回不是raise Exception而是返回{error: xxx}让 Agent 能统一处理结果必须可读eval返回的是 float但用户需要“计算结果3.14”这是 UX 层面的契约。当你把这个calculate加入tools列表再跑run_agent(计算 3.14*21, [weather_tool, calc_tool], client)你会亲眼看到LLM 如何生成{tool_calls: [{name: calculate, args: {expression: 3.14*21}}]}你的代码如何安全执行结果如何注入下一轮。这个闭环比看一百页 LangChain 文档都管用。3. RAG 不是魔法盒知识库的本质是“结构化查询路由”几乎所有新手在学 AI Agent 时都会被 RAGRetrieval-Augmented Generation这个词吓住。教程里说“接入 RAG让 Agent 拥有私有知识”然后就是Chroma.from_documents()VectorStoreRetriever一通配置。结果跑起来用户问“公司报销流程”Agent 却返回“根据《员工手册》第3章……”而手册里根本没有这一章——因为向量检索召回了语义相似但内容错误的片段。RAG 的核心从来不是“向量相似度”而是查询路由Query Routing。它解决的问题是“当用户提问时我该去哪份文档里找答案” 这个决策比检索本身更重要。而新手一上来就堆向量库等于没装导航就开车上高速。3.1 三种知识源三种路由策略知识类型特征最佳路由方式新手常见错误结构化知识如数据库、API字段明确、关系清晰用户表→订单表→商品表SQL 查询或 GraphQL 请求用向量检索查“订单总金额”结果召回一堆“订单状态变更日志”半结构化知识如 Markdown 文档、PDF有标题层级、列表、表格但非数据库基于标题/章节的精确匹配 向量微调把整篇《报销制度》PDF 塞进向量库问“差旅标准”召回“办公用品采购流程”非结构化知识如客服对话记录、邮件无固定格式纯文本流水账关键词正则初筛 向量精排直接向量化百万条聊天记录检索慢且不准我做过一个真实案例某电商公司想让 Agent 回答“退货政策”。他们最初用 Chroma 向量化全部客服对话结果用户问“退货要寄回运费谁付”Agent 从一条“用户抱怨快递慢”的对话里召回“运费”回答“快递公司承担”彻底错误。后来我们改用路由策略先用规则匹配关键词“退货”、“运费”、“寄回”触发后精准查询《售后服务政策》Markdown 文档的“费用承担”章节再对这个章节做小范围向量检索定位到“因商品质量问题退货运费由平台承担”这一句。路由前置检索后置。这才是 RAG 的正确打开方式。LangChain 的MultiVectorRetriever或ParentDocumentRetriever本质都是在实现不同粒度的路由而不是单纯提升向量精度。3.2 动手实现一个轻量级路由引擎不用引入任何框架15 行代码就能实现一个基于规则的路由class KnowledgeRouter: def __init__(self): self.routes [ # 路由规则关键词 → 知识源标识 ([报销, 费用, 发票], policy_reimbursement.md), ([退货, 退款, 寄回], policy_return.md), ([登录, 密码, 重置], faq_auth.md), ([API, 接口, 调用], dev_api_docs.json) ] def route(self, query: str) - str: 根据查询关键词返回最匹配的知识源文件名 query_lower query.lower() for keywords, source in self.routes: if any(kw in query_lower for kw in keywords): return source return general_knowledge.md # 默认源 # 使用示例 router KnowledgeRouter() print(router.route(退货时运费谁承担)) # 输出: policy_return.md print(router.route(如何重置管理员密码)) # 输出: faq_auth.md这个KnowledgeRouter就是你 Agent 的“知识地图”。它不关心向量距离只关心语义意图。当 Agent 收到用户问题先调router.route(query)拿到policy_return.md再去这个文件里做精准检索甚至直接全文搜索“运费”效率和准确率远超全库向量化。注意RAG 的瓶颈90% 不在向量模型而在路由策略的粗糙。一个if 报销 in query能解决 60% 的问题剩下的 40% 才需要 BERT 或 ColBERT。别本末倒置。4. MCP被严重低估的“Agent 通信协议”本质最近热词里反复出现的MCPModel Context Protocol很多人以为是某个新框架或工具甚至搜到unreal 5.8 mcp或altium designer ai接口 mcp误以为是行业专用协议。其实MCP 的本质极其朴素它是一套定义“模型如何与外部世界交换结构化数据”的 JSON Schema 标准。就像 HTTP 是浏览器和服务器的通信协议MCP 是 LLM 和工具、知识库、其他 Agent 之间的通信协议。4.1 MCP 的核心不是技术是契约精神MCP 规范里最关键的不是某个字段怎么写而是它强制要求所有参与方LLM、Tool、Orchestrator必须共享同一份 Schema 定义。例如一个标准的 MCP Tool Call 消息长这样{ type: tool_call, id: call_abc123, name: get_weather, arguments: {city: 北京} }而对应的 Tool Response 必须是{ type: tool_result, id: call_abc123, result: {temperature: 25, condition: 晴, humidity: 45%} }注意id字段——它像快递单号确保tool_result能精准匹配到哪个tool_call。没有这个 ID当 Agent 并发调用 3 个工具时结果就会错乱。LangChain 的Tool类里return_direct参数、Dify 的“工具调用 ID 绑定”底层都是在模拟 MCP 的id机制。我见过最典型的坑一个学员用 CrewAI 写多 Agent 协作Researcher Agent 调用爬虫Writer Agent 等结果。结果 Writer 总是拿到 Researcher 上次的结果。排查三天才发现两个 Agent 的工具调用没有唯一 ID结果被混在一起了。加一行uuid.uuid4().hex生成tool_id问题立刻解决。这不是框架 bug是你没理解 MCP 的“契约”本质——ID 是保证数据不混淆的底线。4.2 用 Python 实现 MCP 兼容的工具层既然 MCP 是 JSON Schema我们就用 Python 的pydantic来强制校验让工具天然兼容from pydantic import BaseModel, Field from typing import Optional, Dict, Any import uuid class MCPToolCall(BaseModel): type: str Field(tool_call, constTrue) id: str Field(default_factorylambda: uuid.uuid4().hex) name: str arguments: Dict[str, Any] class MCPToolResult(BaseModel): type: str Field(tool_result, constTrue) id: str result: Optional[Any] None error: Optional[str] None def mcp_compatible_tool(func): 装饰器将普通函数包装为 MCP 兼容工具 def wrapper(mcp_call: MCPToolCall) - MCPToolResult: try: # 安全执行 result func(**mcp_call.arguments) return MCPToolResult(idmcp_call.id, resultresult) except Exception as e: return MCPToolResult(idmcp_call.id, errorstr(e)) return wrapper mcp_compatible_tool def get_weather(city: str) - dict: return {temperature: 25, condition: 晴} # 使用 call MCPToolCall(nameget_weather, arguments{city: 北京}) result get_weather(call) # 返回标准 MCPToolResult这个mcp_compatible_tool装饰器做了三件事输入强制为MCPToolCall确保id和arguments结构正确输出强制为MCPToolResult自带id回传错误统一捕获为error字段不抛异常。从此你的工具天然支持任何遵循 MCP 的 Agent 框架。不需要改一行 LangChain 配置也不用等 Dify 更新 SDK——因为协议在代码里不在框架里。5. 框架选型真相LangChain、CrewAI、Dify 的能力边界图谱当你的手写 Agent 能稳定跑通计算器、天气、知识路由后才该考虑框架。但选框架不是“哪个好”而是“哪个能补足你当前最痛的短板”。我把主流框架按核心能力拆解成一张能力图谱帮你避开宣传话术直击本质。5.1 LangChain最强的“胶水层”最弱的“工程约束”LangChain 的本质是Python 生态的 Adapter 模式集合。它把requests、chromadb、openai、llama_cpp等几十个库用一套统一的Runnable接口粘起来。它的优势是“什么都能接”劣势是“什么都要你管”。能力维度LangChain 表现对新手的真实影响工具集成提供StructuredTool、Tool抽象但需手动实现invoke()你仍要写if tool.name weather的分支逻辑没省事状态管理RunnableWithMessageHistory可存对话但不自动关联 tool_id多轮工具调用结果易丢失需自己维护state字典错误处理RetryPolicy可重试但不区分网络错误 vs 模型幻觉天气 API 超时和模型编造温度都被当成同一类错误重试部署langchain-server可启 API但无鉴权、无限流、无监控直接暴露给前端被恶意刷 tokenLangChain 适合已有成熟工具链只需快速串联。比如你公司已有内部天气 API、报销系统 DB、客服知识库LangChain 就是最佳胶水。不适合从零开始建 Agent指望它帮你设计架构。5.2 CrewAI专为“多角色协作”设计的流程引擎CrewAI 的核心创新是把 Agent 当成“岗位”而非“函数”。它预设了Agent角色、Task职责、Process协作流程三层抽象。这解决了 LangChain 最大的痛点如何让多个 Agent 有序协作而不是各自为战。from crewai import Agent, Task, Crew researcher Agent( role市场研究员, goal收集竞品最新动态, tools[serp_search, pdf_reader] ) writer Agent( role内容撰稿人, goal基于调研结果写公众号推文, tools[markdown_writer] ) task1 Task(description搜索 A 公司 Q3 财报, agentresearcher) task2 Task(description写一篇 800 字分析文章, agentwriter, context[task1]) crew Crew(agents[researcher, writer], tasks[task1, task2]) result crew.kickoff() # 自动调度researcher 先跑结果传给 writerCrewAI 的context[task1]就是显式声明数据依赖。它内置了SequentialProcess顺序执行和HierarchicalProcess经理分配任务比 LangChain 的RouterChain更贴近真实工作流。但代价是它假设所有 Agent 都用同一套 LLM无法混合使用本地 phi3 和云端 gpt-4。如果你需要异构模型调度CrewAI 反而成了枷锁。5.3 Dify面向产品的“低代码 Agent 工厂”Dify 的定位很清晰让产品经理、运营、销售不写代码也能搭 Agent。它的 UI 里“知识库”、“插件”、“工作流”全是可视化拖拽。但背后它强制你接受它的架构知识库必须用它内置的向量引擎不支持你自己的 Chroma插件必须按它的Plugin Schema开发JSON-RPC over HTTP工作流节点是黑盒你无法 debug 某个节点的中间输出。Dify 的价值在于把 80% 的重复劳动标准化用户认证、token 计费、对话历史存储、Webhook 集成。如果你要做一个对外服务的客服 AgentDify 能省下 3 人月的后端开发。但如果你想研究“LLM 在工具调用失败后的自我修复机制”Dify 的黑盒会让你寸步难行。我的建议用 LangChain 打地基理解协议用 CrewAI 试多角色理解协作最后用 Dify 上线交付产品。三者不是替代关系而是能力叠加。6. 转行者生存指南从“写代码”到“建系统”的思维跃迁我辅导的转行者中最成功的那批人都不是 Python 写得最好的而是最早完成思维跃迁的。他们不再问“这个函数怎么用”而是问“这个组件在系统里承担什么责任它的失败会引发什么连锁反应”。这种系统思维才是 AI Agent 工程师的核心壁垒。6.1 用“故障树”代替“报错日志”新手看日志KeyError: tool_calls→ 谷歌搜解决方案 → 改response_format。老手看日志KeyError: tool_calls→ 画故障树模型没输出tool_calls→ 是 prompt 没写清楚还是 temperature 太高模型输出了但字段名不对 → 是工具定义和 prompt 描述不一致模型输出正确但代码没解析 → 是json.loads()后取 key 的逻辑错了我让学员强制用纸笔画故障树哪怕只有 3 层。结果发现80% 的“疑难杂症”根源都在第一层prompt 没约束模型输出格式。比如你写“请调用工具”模型可能返回{action: weather, params: {...}}但你的代码只认{tool_calls: [...]}。加一句“请严格按以下 JSON 格式输出{...}”问题消失。6.2 “Token 是成本不是单位”新手算 tokenlen(prompt.encode(utf-8))→ “哦用了 500 token”。老手算 token500 token × $0.002/1K $0.001→ “这个 Agent 每次调用烧掉 1 分钱日活 1 万就是 100 块够买 200G SSD 了”。更进一步老手会做 token 优化Prompt 里删掉所有“你好”、“请”、“谢谢”等礼貌词LLM 不需要被礼貌对待工具描述只留name、description、parameters删掉示例示例占 30% token用json.dumps(..., separators(,, :))压缩 JSON 输出减少空格换行。一个简单的separators参数能让tool_calls输出从 120 token 降到 85 token省下 30% 成本。这不是炫技是职业素养。6.3 拒绝“框架依赖症”建立“能力坐标系”最后送你一张我自用的“AI Agent 能力坐标系”横轴是抽象层级从 raw Python 到 Dify UI纵轴是控制粒度从全局流程到单个 token。你的目标是让自己始终处于“能向下穿透也能向上封装”的交叉点。高控制粒度细 │ │ 手写 Agent 循环 → 工具契约 → Token 级 prompt 优化 │ │ │ │ LangChain Runnable → 自定义 Callback → 拦截中间输出 │ │ │ └── CrewAI Agent → 修改 _execute_task 源码 → 注入自定义逻辑 │ │ Dify 工作流 → 用 Custom Plugin → 调用自有 API │ 低控制粒度粗──────────────────────────────────────→ 高抽象层级永远不要停留在“只会用 Dify 拖拽”的右下角。每次用框架都问自己“如果这个功能失效了我能用 Python 哪几行代码临时顶上” 答案越具体你的护城河就越深。我在实际项目中发现真正决定项目成败的往往不是模型多强大而是当get_weather工具超时时Agent 是优雅降级返回“正在查询”还是直接崩掉。这种鲁棒性不来自框架文档来自你亲手写过十次try/except亲手测过五种超时策略亲手看过三次curl -v的原始响应头。这才是转行者最该投资的时间。所以别急着啃框架。先让那个 87 行的手写 Agent在你的终端里稳稳跑起来。当它第一次正确调用计算器返回“计算结果7”那一刻你就已经站在了 AI Agent 工程师的起跑线上。
阅读完成 · 觉得有帮助?
咨询建站