1. 从四个名词说起:为什么你总在概念里打转
刚接触AI应用开发那阵子,我最怕被人问“你搞的到底是Agent还是Prompt”。不是答不上来,是每次答完,对方下一句准是“那跟MCP有啥区别”。这四个词——MCP、Skill、Agent、Prompt——像四块拼图,单独看都认识,拼一起就懵。更麻烦的是,网上讲它们的文章要么纯概念,要么直接甩代码,中间那层“到底怎么串起来”的实操逻辑,几乎没人讲透。
这篇东西就是来填这个坑的。我会按一个真实项目从零到跑通的顺序,把四个概念拆开揉碎,再拼回一个能用的数据分析场景里。你如果是刚转AI应用方向的开发者,或者已经在写Prompt但总觉得“差点意思”的工程师,再或者带团队做智能化改造的技术负责人,这篇应该能帮你省下至少两周的试错时间。核心就一句话:Prompt是话术,Skill是手艺,Agent是掌柜,MCP是柜台。下面挨个拆。
先给个全局定位,免得后面绕晕。Prompt是你对模型说的那句话;Skill是模型或系统掌握的一项具体能力,比如“会算同比环比”;Agent是能自己决定“先干啥再干啥”的执行者;MCP则是让Agent能跟外部系统(数据库、API、文件)说上话的协议层。四者不是并列关系,是层层包裹的依赖关系。搞不清这个,写出来的东西要么是“只会聊天的玩具”,要么是“跑一次就崩的脚本”。
我见过太多项目死在“概念混用”上。比如有人把一堆Prompt硬塞进一个函数里,管这叫Agent;也有人明明只需要一个固定Skill,非得上Agent框架,结果调试成本翻三倍。所以第一部分我不急着讲怎么搭,先把每个概念的边界划清楚,再讲它们怎么咬合。你耐心看完这一节,后面实操部分会顺很多。
1.1 Prompt:不是“提示词”,是“接口契约”
很多人把Prompt叫“提示词”,这名字容易让人以为它是随便写的一句话。实际上在工程化场景里,Prompt是一份接口契约——你告诉模型“输入是什么格式、输出要什么结构、遇到异常怎么处理”。写得好不好,直接决定下游代码能不能稳定解析。
我拿数据分析场景举个例。假设你要模型从一段销售描述里抽“地区、品类、金额”三个字段。新手会写:“请从下面文字里提取地区、品类和金额。”老手会写:
你是一个数据抽取器。输入是一段中文销售记录。 输出必须是JSON,格式为: {"region": "字符串", "category": "字符串", "amount": 数字} 规则: 1. 金额只保留数字,去掉“元”“万”等单位,万要乘以10000。 2. 如果地区缺失,填“未知”。 3. 只输出JSON,不要任何解释。 输入:{input_text}差别在哪?前者模型可能回你“好的,地区是华东,品类是家电,金额是五千元”,你还得再写正则去抠。后者直接给你{"region":"华东","category":"家电","amount":5000},json.loads一把过。Prompt的工程价值不在于“让模型听懂”,而在于“让代码不用猜”。
这里有个我踩过的坑:早期我写Prompt喜欢加“请”“麻烦”“谢谢”,觉得礼貌点模型表现好。实测下来,在结构化抽取任务里,这些词反而增加输出噪声。后来我把所有客套话删掉,换成“必须”“只能”“禁止”这类硬约束,解析成功率从七成出头拉到九成五以上。当然,创意类任务另说,但数据分析这种要精确结果的场景,Prompt越像“机器指令”越稳。
还有一个关键点:Prompt要版本化。你今天调好一版,明天模型更新了,输出可能就变了。我的做法是把Prompt存成独立文件,比如prompts/extract_sales_v3.txt,每次改动记下日期和变更原因。别小看这个习惯,线上出问题时,你能五分钟回滚到上一版,而不是对着代码干瞪眼。
1.2 Skill:把“会做”变成“可复用”
Skill这个词在不同框架里定义不一样,但本质就一个:封装好的、可被反复调用的能力单元。它比Prompt高一层,因为Skill通常包含“Prompt + 后处理 + 异常兜底”的完整逻辑。
还是拿数据分析举例。“算同比增长率”这件事,如果每次都写一遍Prompt让模型算,又慢又不准。正确做法是把它做成一个Skill:输入本期值和同期值,内部先让模型确认单位是否一致,再用代码算(本期-同期)/同期,最后格式化成百分比。这样调用方只需要传两个数字,不用关心里面怎么实现的。
Skill的设计有三个原则我反复验证过:
- 单一职责:一个Skill只干一件事。“清洗数据并算同比并画图”这种三合一Skill,调试时你会想砸键盘。
- 输入输出强类型:别用“一段文字进、一段文字出”。用JSON Schema或者Pydantic模型定义清楚,比如
SalesInput(region: str, amount: float)。 - 自带降级策略:模型调用失败怎么办?返回缓存?走规则兜底?还是抛异常让上层处理?这个必须在Skill内部想清楚,不能甩给调用方。
我做过一个对比实验:同一个“从财报文本抽关键指标”的任务,用纯Prompt方案和Skill封装方案各跑100次。纯Prompt方案平均耗时4.2秒,成功率78%;Skill方案因为加了格式校验和重试,平均耗时5.1秒,但成功率94%。多花的那0.9秒,换来的是不用人工复核,这笔账怎么算都值。
注意:Skill不是越多越好。我见过一个项目堆了四十多个Skill,结果Agent选错Skill的概率比选对还高。后来砍到十二个核心Skill,整体准确率反而上去了。Skill的粒度控制在“一个业务动作”级别最合适,太细会碎片化,太粗会难复用。
1.3 Agent:从“执行者”到“决策者”的分水岭
Agent和前面两个最大的区别是:它自己决定下一步做什么。Prompt和Skill都是被动的——你调它才动。Agent是主动的——你给个目标,它自己拆步骤、选工具、看结果、再决定继续还是重来。
数据分析场景里,一个典型的Agent任务长这样:用户说“帮我看看上个月华东区为什么销量下滑”。Agent会自己规划:先查数据库拿上个月和上上个月的华东区销量,再按品类拆解看哪个品类拖后腿,然后对比促销活动记录,最后生成一段归因分析。整个过程里,没有人告诉它“先查A再查B”,是它根据中间结果动态决定的。
但Agent不是银弹。我强烈建议:能用固定流程解决的,别上Agent。因为Agent的调试成本是指数级的——它可能走进死循环、可能选错工具、可能把简单问题复杂化。我自己的判断标准是:如果任务的步骤数量固定且顺序明确,用Skill编排就够了;只有当“下一步取决于上一步的结果”且“步骤数量不固定”时,才考虑Agent。
Agent的核心组件其实就四个:规划器、工具集、记忆、执行循环。规划器负责拆任务,工具集就是它能调用的Skill和MCP接口,记忆存中间结果和上下文,执行循环控制“想-做-看”的节奏。这四个里最容易出问题的是记忆——上下文太长会超token限制,太短会丢关键信息。我的经验是给记忆设一个“滑动窗口+摘要”机制:最近五轮对话保留原文,更早的压缩成一段摘要。
1.4 MCP:让Agent长出“手和脚”
MCP全称是Model Context Protocol,你可以把它理解成Agent和外部世界之间的标准插座。没有MCP的时候,Agent要连数据库得写一套代码,要连文件系统得再写一套,要连内部API又得写一套。每接一个新系统,就是一次重复劳动。MCP把这些连接抽象成统一协议,Agent只需要说“我要调这个工具”,具体怎么连、怎么传参、怎么处理返回,由MCP层搞定。
在数据分析项目里,MCP的价值特别明显。你的Agent可能需要同时访问:MySQL里的业务数据、S3上的CSV文件、内部报表系统的API、还有本地的一个Excel模板。没有MCP,你得写四套适配代码;有了MCP,你只需要配置四个MCP Server,Agent端统一用call_tool(name, params)调用。
但MCP也不是没有代价。它多了一层通信,延迟会增加。我实测过,直接调本地函数大概2毫秒,走MCP大概15到30毫秒。对于数据分析这种秒级任务,这点延迟无所谓;但如果是高频交易场景,就得掂量了。另外MCP Server的稳定性直接影响Agent——Server挂了,Agent就“断手断脚”。所以生产环境里,MCP Server必须做健康检查和自动重启。
提示:MCP的配置建议从简开始。先接一个最核心的数据源,跑通全链路,再逐步加。我见过一上来配八个MCP Server的项目,光调试连接就花了一周,业务逻辑还没开始写。
2. 四者如何咬合:一个数据分析项目的完整拆解
概念讲完了,但光知道定义没用。这一部分我拿一个真实做过的项目——“销售异动归因分析”——把四个东西怎么串起来讲清楚。项目背景很简单:业务方每天要看“哪个区、哪个品类、哪家门店的销量跟预期偏差超过20%”,然后自动生成一段归因说明。听起来不复杂,但里面Prompt、Skill、Agent、MCP全用上了。
2.1 整体架构:谁在哪个位置干活
先看全局数据流。用户输入一句话:“分析昨天销售异动。”这句话进到系统后,经历这么几个阶段:
- Agent接收目标,开始规划。它先判断需要哪些数据:昨日销量、预期销量、维度拆解(区域/品类/门店)。
- Agent通过MCP调用数据接口,拿到原始数据。这里MCP Server封装了数据库查询逻辑,Agent不需要知道底层是MySQL还是ClickHouse。
- Agent调用Skill做计算。比如“偏差计算Skill”接收实际值和预期值,返回偏差百分比和是否超阈值。
- Agent根据中间结果决定下一步。如果发现某个区域偏差大,它会继续下钻到品类;如果偏差分散,它可能直接进入归因生成。
- 归因生成Skill被调用,内部用Prompt让模型写一段人话解释,同时附上关键数字。
- Agent汇总结果,通过MCP把报告写到内部系统,同时返回给用户。
这个流程里,Prompt只在第5步出现,而且是被Skill封装着用的。Skill出现在第3和第5步,负责具体计算和生成。Agent贯穿全程,是总调度。MCP在第2和第6步,负责跟外部系统打交道。你看,四者各司其职,没有谁替代谁。
我特意把Prompt“藏”在Skill里面,而不是让Agent直接拼Prompt。为什么?因为Agent直接拼Prompt的话,每次生成的Prompt可能都不一样,输出格式就不稳定。封装成Skill后,Prompt是固定的、版本化的,Agent只需要传参数。这个设计决策让我们的输出格式一致率从65%提到了98%。
2.2 关键设计决策:为什么这么选
架构定下来之前,我试过另外两种方案,都失败了。第一种是“全Prompt方案”:把所有逻辑写在一个巨型Prompt里,让模型一次性输出分析结果。问题是模型算数不准,而且没法访问实时数据。第二种是“全硬编码方案”:用if-else写死所有分析路径。问题是业务规则一变就得改代码,维护成本太高。
最终选的“Agent+Skill+MCP”方案,核心权衡就三点:
- 灵活性 vs 稳定性:Agent提供灵活性(能处理没预设过的情况),Skill提供稳定性(关键计算不靠模型)。两者结合,既不会太死,也不会太飘。
- 开发速度 vs 运行成本:MCP接入新数据源很快,但每次调用有延迟。我们的折中是:高频查询走缓存,低频查询走MCP实时拉。
- 模型能力 vs 代码逻辑:需要“理解”和“生成”的环节交给模型(归因文案),需要“精确”和“可复现”的环节交给代码(偏差计算)。
这里有个参数值得展开说:偏差阈值设为20%。为什么是20%而不是10%或30%?我们拿历史数据跑了一遍,10%的话每天触发两百多条告警,业务方根本看不过来;30%的话又会漏掉一些早期信号。20%是“告警量在可处理范围内”和“不漏关键异动”的平衡点。这个数不是拍脑袋定的,是拿三个月历史数据回测出来的。
2.3 数据流转的细节:从一句话到一份报告
具体走一遍。用户输入“分析昨天销售异动”后,Agent的第一反应不是直接查数据,而是先做意图澄清。它会检查:用户有没有指定区域?有没有指定品类?如果都没指定,默认全区域全品类。这个澄清逻辑本身也是一个Skill,叫“意图解析Skill”。
然后Agent规划出三步:第一步拉数据,第二步算偏差,第三步生成归因。注意,这个规划不是写死的,是Agent根据当前上下文动态生成的。如果用户说“分析昨天华东区销售异动”,规划就变成:拉华东区数据、按品类拆解、算各品类偏差、生成归因。步骤数量从三变成四。
拉数据的时候,Agent通过MCP调用query_sales_data工具,参数是{date: "昨天", region: "华东", metrics: ["actual", "forecast"]}。MCP Server收到后,翻译成SQL去查库,返回一个JSON数组。这里有个细节:MCP返回的数据结构必须和Skill的输入结构对齐,否则Agent还得做一次转换。我们的做法是在MCP Server里就把字段名统一成Skill约定的格式,省掉中间层。
算偏差的时候,“偏差计算Skill”被调用。它接收实际值和预期值列表,返回每个维度的偏差百分比和是否超阈值。这个Skill内部没有用模型,纯Python计算。为什么?因为模型算除法会出错,而且同样的输入每次输出可能不一样。精确计算必须用代码,这是底线。
生成归因的时候,“归因生成Skill”被调用。它把偏差数据整理成一段结构化文本,塞进Prompt模板,让模型写一段解释。Prompt里明确要求:“只基于给定数据,不要编造原因。如果数据不足以解释,就说‘需要进一步下钻’。”这个约束很重要,否则模型会开始“脑补”市场环境、竞争对手之类的信息。
最后,Agent把归因文本和关键数字汇总,通过MCP的write_report工具写到内部系统。整个链路跑下来,平均耗时8到12秒,其中模型调用占3到5秒,MCP通信占1到2秒,剩下是计算和编排。
3. 实操落地:从零搭一个最小可用版本
理论讲再多,不如跑通一个demo。这一部分我带你搭一个最小版本,只做一件事:从一段销售文本里抽取数据,算偏差,生成一句话归因。不需要数据库,不需要复杂框架,用Python加一个模型API就能跑。目的是让你亲手感受四个概念怎么落地。
3.1 环境准备与依赖选择
我选Python 3.10以上,因为要用到一些类型注解的新特性。核心依赖就三个:openai(调模型)、pydantic(定义数据结构)、requests(调HTTP接口)。如果你用其他模型,把openai换成对应的SDK就行,逻辑一样。
pip install openai pydantic requests目录结构建议这样:
project/ prompts/ extract_sales.txt generate_attribution.txt skills/ extract_skill.py deviation_skill.py attribution_skill.py mcp_servers/ sales_data_server.py agent.py main.py为什么把Prompt单独放文件?前面说过,版本化。为什么Skill一个文件一个?方便单独测试。MCP Server也独立,因为生产环境里它可能跑在另一台机器上。
注意:别一上来就上LangChain或者AutoGPT这类框架。我试过,对于这种明确流程的任务,框架带来的抽象层反而增加调试难度。先用原生代码跑通,再考虑要不要上框架。
3.2 Prompt模板的编写与调优
先写extract_sales.txt。这个Prompt的任务是从一段中文描述里抽销售数据。我调了七版才稳定,最终版长这样:
你是一个销售数据抽取器。输入是一段中文销售记录。 输出必须是JSON数组,每个元素格式为: {"region": "字符串", "category": "字符串", "actual": 数字, "forecast": 数字} 规则: 1. 金额单位统一为元。如果原文是“万”,乘以10000。 2. 如果某个字段缺失,填null。 3. 只输出JSON,不要任何解释、不要markdown代码块。 4. 如果输入无法解析出任何记录,输出空数组[]。 输入:{input_text}关键调优点:第3条“不要markdown代码块”。早期版本模型总爱输出json ...,导致json.loads失败。加上这句后,直接输出纯JSON。第4条空数组兜底也很重要,否则模型遇到无关输入会瞎编。
再写generate_attribution.txt:
你是一个销售分析师。根据以下偏差数据,写一段不超过100字的归因说明。 要求: 1. 只基于给定数据,不要编造外部原因。 2. 如果偏差超过20%,指出具体维度和方向。 3. 如果数据不足以判断原因,直接说“需要进一步下钻”。 偏差数据:{deviation_json}这个Prompt的调优重点是**“不要编造外部原因”**。不加这句,模型会写“可能是受季节因素影响”之类的废话。加了之后,输出干净很多。
3.3 Skill的封装与测试
extract_skill.py的核心逻辑:
import json from openai import OpenAI client = OpenAI() def extract_sales(input_text: str) -> list[dict]: with open("prompts/extract_sales.txt") as f: template = f.read() prompt = template.replace("{input_text}", input_text) response = client.chat.completions.create( model="gpt-4o-mini", messages=[{"role": "user", "content": prompt}], temperature=0 ) raw = response.choices[0].message.content.strip() try: data = json.loads(raw) if not isinstance(data, list): return [] return data except json.JSONDecodeError: return []注意temperature=0,这是为了输出稳定。还有异常兜底:解析失败返回空列表,不让上层崩掉。
deviation_skill.py纯计算:
def calc_deviation(records: list[dict]) -> list[dict]: results = [] for r in records: actual = r.get("actual") forecast = r.get("forecast") if actual is None or forecast is None or forecast == 0: continue dev = (actual - forecast) / forecast results.append({ "region": r.get("region"), "category": r.get("category"), "deviation": round(dev, 4), "exceed": abs(dev) > 0.2 }) return results这个Skill没有任何模型调用,纯代码。为什么?因为除法必须精确,而且同样的输入必须每次输出一样。凡是能用代码算的,绝不用模型。
attribution_skill.py:
def generate_attribution(deviations: list[dict]) -> str: exceed_items = [d for d in deviations if d["exceed"]] if not exceed_items: return "昨日销售无显著异动。" with open("prompts/generate_attribution.txt") as f: template = f.read() prompt = template.replace("{deviation_json}", json.dumps(exceed_items, ensure_ascii=False)) response = client.chat.completions.create( model="gpt-4o-mini", messages=[{"role": "user", "content": prompt}], temperature=0.3 ) return response.choices[0].message.content.strip()这里temperature=0.3,因为归因文案需要一点自然语言的灵活性,但又不能太飘。0.3是试出来的,0的话文案太干,0.7的话开始编原因。
3.4 MCP Server的模拟与接入
真实项目里MCP Server要连数据库,但demo里我用一个本地JSON文件模拟。sales_data_server.py:
import json def query_sales_data(date: str, region: str = None) -> list[dict]: with open("mock_data/sales.json") as f: data = json.load(f) results = [d for d in data if d["date"] == date] if region: results = [d for d in results if d["region"] == region] return results生产环境里,这个函数会换成数据库查询,外面包一层MCP协议。但核心逻辑一样:接收结构化参数,返回结构化数据。Agent不需要知道数据从哪来,只需要知道调这个工具能拿到什么。
3.5 Agent主循环的编写
agent.py是总调度:
def run_agent(user_input: str): # 第一步:抽取数据 records = extract_sales(user_input) if not records: return "未能从输入中解析出销售数据。" # 第二步:算偏差 deviations = calc_deviation(records) # 第三步:生成归因 attribution = generate_attribution(deviations) return { "records": records, "deviations": deviations, "attribution": attribution }你看,这个“Agent”其实就是一个顺序调用。为什么这么简单?因为demo的任务步骤是固定的。真实Agent的“动态规划”能力,是在步骤不固定时才需要的。先跑通固定流程,再考虑动态规划,这是我的建议。
跑一下:
result = run_agent("昨天华东区家电实际卖了120万,预期100万;华南区服装实际80万,预期95万。") print(result["attribution"])输出大概是:“华东区家电偏差+20%,需关注;华南区服装偏差-15.8%,未超阈值。”这个结果不算惊艳,但链路是通的。你可以在这个基础上加数据库、加更多Skill、加动态规划,逐步逼近生产级。
4. 踩坑记录与排查手册
这一部分是我做这类项目两年多攒下来的“血泪清单”。每个坑都真实踩过,每个解法都验证过。
4.1 常见问题速查表
| 现象 | 可能原因 | 排查步骤 | 解法 |
|---|---|---|---|
| 模型输出带markdown代码块 | Prompt没约束格式 | 打印原始输出 | Prompt加“不要markdown代码块” |
| JSON解析失败 | 模型输出多余文字 | 看raw内容 | 加“只输出JSON”约束,加异常兜底 |
| 偏差计算结果每次不一样 | 用了模型算数 | 检查Skill是否调模型 | 改成纯代码计算 |
| Agent陷入死循环 | 规划器没有终止条件 | 打印每轮决策 | 设最大轮次限制,比如10轮 |
| MCP调用超时 | Server没做超时处理 | 看Server日志 | 加超时和重试,设3秒超时 |
| 归因文案编造原因 | Prompt没限制 | 看输出是否含外部信息 | 加“只基于给定数据” |
| Skill选错 | Skill描述太模糊 | 看Agent选的哪个 | 给每个Skill写清晰描述和示例 |
| 上下文超token | 记忆没压缩 | 看token计数 | 滑动窗口+摘要 |
4.2 三个最容易翻车的细节
第一个:Prompt里的变量替换。我早期用str.replace,结果输入文本里刚好有{input_text}这个字符串,替换就乱了。后来改用string.Template或者直接f-string拼接,但f-string又有转义问题。最终方案是用Jinja2模板,变量用{{ input_text }},冲突概率低很多。
第二个:MCP的序列化。MCP传输的数据必须是可序列化的。我试过传一个Pydantic对象,结果报错。后来统一在MCP边界做model_dump()转成dict,再序列化。MCP层只传基础类型:字符串、数字、布尔、列表、字典,别传自定义对象。
第三个:Agent的“幻觉工具调用”。Agent有时候会调用一个不存在的工具名,比如query_sales写成query_sale。这个在MCP层要做校验:收到未知工具名直接返回错误,让Agent重新规划。同时给Agent的工具列表要精确,别用模糊描述。
4.3 性能优化的三个实操技巧
技巧一:Prompt缓存。同样的Prompt前缀,如果模型支持缓存(比如某些API的system prompt缓存),能省不少token和时间。我把所有Skill的system prompt统一成一段固定文本,实测首token延迟降低约30%。
技巧二:并行调用。如果Agent需要拉多个独立数据源,别串行。用asyncio.gather并行调MCP,总耗时从“各耗时之和”变成“最大耗时”。我有个任务从12秒降到5秒,就是靠这个。
技巧三:结果缓存。数据分析场景里,很多查询是重复的。比如“昨天华东区销量”可能被多个分析任务用到。我在MCP Server层加了一层Redis缓存,key是查询参数的哈希,ttl设5分钟。命中率大概四成,整体响应快了不少。
提示:缓存要设过期时间,别用永久缓存。数据是会变的,缓存太旧会导致分析结果错误。5分钟是个比较安全的窗口。
4.4 从demo到生产的差距
demo跑通只是第一步。生产环境还要考虑:并发请求怎么处理?模型API限流怎么办?MCP Server挂了怎么降级?日志和监控怎么做?这些每一个都是大话题,但核心思路就一条:每个环节都要有兜底。模型调用失败走规则兜底,MCP超时走缓存兜底,Agent规划失败走固定流程兜底。别指望任何一个环节永远不出问题。
我个人在实际操作中的体会是,这类项目最难的不是写代码,是定义清楚每个概念的边界。Prompt就干Prompt的事,别让它兼做计算;Skill就干Skill的事,别让它兼做调度;Agent就干Agent的事,别让它兼做数据连接。边界清晰了,调试成本会低一个数量级。我见过太多项目因为边界模糊,最后变成一坨“什么都干但什么都干不好”的代码。
最后再分享一个小技巧:每次改完Prompt或Skill,跑一遍回归测试集。我维护了一个50条输入的测试集,覆盖正常、边界、异常三种情况。改完跑一遍,看成功率有没有下降。这个习惯帮我拦住了至少五次“改一个坏三个”的线上事故。测试集不用大,但要稳定,别每次换。