关于 icp-builder
将 LinkedIn 个人资料转化为 persona 和推断所得 ICP 配置文件的技能,供相关销售技能使用。它基于 Crustdata 数据和帖子提取信息,并写入可复用的 GTM 设置。
主要功能
- 01基于 LinkedIn 创建 persona
- 02根据个人与公司信息推断 ICP
- 03从帖子分析写作风格
- 04可选配置销售工具栈
- 05生成可复用的 GTM 配置文件
适用场景
- 01为寻客工作流设置 GTM 背景
- 02根据销售人员资料创建推断所得 ICP
- 03从帖子提取用户的写作风格
- 04为账户调研技能准备配置文件
MCP 相关
通过 `execute` 脚本调用 Crustdata MCP 工具,丰富人员和公司信息并获取社交帖子。
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ crustdata/ skills --skill icp-builder安装后在 Claude Code 中输入/icp-builder确认已加载。
附带文件evals/evals.jsonREADME.md
详细介绍
ICP 构建器
输入一个 LinkedIn 网址,输出可用的 GTM 配置。此技能通过 Crustdata 丰富用户自己的个人资料,并写入 config/persona-profile.md + config/gtm-config.md ——
sales-prospecting 和 account-research 会在启动时读取这些文件。
始终按以下顺序执行三个步骤:
- 技术栈(可选,可完全跳过):他们使用哪些工具。
- 人物画像:一个 LinkedIn 网址;Crustdata 会据此生成他们的身份、销售的产品、推断出的 ICP,以及他们的写作风格。
- 写入配置并交接。
绝不盘问用户。 不要问“你卖什么”“你的 ICP 是谁”或“粘贴几封体现你写作风格的邮件”。所有这些信息都从 LinkedIn 网址及其帖子中提取。 这个网址就是完整的访谈。
步骤 0:检查是否已有配置
如果工作目录中已存在 config/gtm-config.md 或 config/persona-profile.md,请读取文件,用两行总结其中的内容,并询问用户是要刷新整个用户画像,还是更新特定字段。绝不要悄悄覆盖用户已经修正过的配置。刷新时,保留现有的技术栈条目,不做更改;除非用户要求修改,否则不要再次询问技术栈。文件缺失是正常情况——此技能会创建这些文件。
步骤 1:欢迎 + 可选的技术栈问题
先用一句简短的欢迎语开场,然后只问一个可选问题:你使用哪些工具? 快速逐项确认;用户可以说出工具名称,也可以跳过。如果用户跳过整个问题,就在所有位置填写 none,然后继续。
- 数据提供商 — Crustdata,即这些技能使用的数据源(作为连接器添加;如果尚未连接,请使用下方的无数据回退方案)
- CRM — 或跳过
- 日历 — 或跳过
- 电子邮件 — 或跳过
- 通话录音工具 — 或跳过
- 序列工具 — 或跳过
- 团队聊天工具 — 或跳过
此步骤的规则:
- 绝不要根据已连接的连接器推断技术栈。 已连接的连接器不代表用户的选择。请询问,或填写
none。 - 每一项都可以跳过;绝不要施压,也不要再次询问用户已拒绝的工具。
- 跳过的项目 = 配置中的
none= 下游技能仅为该项目生成草稿:使用草稿和 CSV 导出,而不是推送到工具中(“为你的序列工具导出 CSV”、“改为记录到文件,而不是 CRM”)。
步骤 2:LinkedIn 网址 → 人物画像
只询问一项:用户的 LinkedIn 网址。然后在一个 execute
脚本中构建人物画像。先查找个人;公司信息丰富和帖子获取都依赖个人查找,但两者互不依赖,因此使用 parallelMap 并行执行。
每个脚本都必须以标明来源的查询注释开头(// user query: ... 或
// model query: ...)——没有此注释的脚本会在运行前被拒绝,且不产生任何费用。
// user query: set up my GTM config — my LinkedIn is https://www.linkedin.com/in/example
const url = "https://www.linkedin.com/in/example";
// Stage 1: the person. Base cost 1 credit. `fields` is a response WHITELIST —
// the result carries ONLY the groups listed here; an omitted group reads as
// undefined later and looks like missing data. basic_profile + experience covers
// the persona; social_handles carries the canonical profile URL the posts pull
// is keyed on; contact groups only add cost.
const pr = await callTool("person_enrich", {
professional_network_profile_urls: [url],
fields: ["basic_profile", "experience", "social_handles"],
});
if (!pr.ok) return { error: pr.message };
const person = pr.data[0]?.matches?.[0]?.person_data;
if (!person) return { error: "no_match" }; // → confirm the URL, then no-data fallback
const canonicalUrl = profileUrl(person) ?? url; // preloaded accessor
const current = person.experience?.employment_details?.current?.[0] ?? {};
const companyId = currentCompanyIds(person)[0]; // preloaded accessor
// Stage 2: company + posts are independent of each other — fan them out.
const calls = [
{ name: "social_post_list_live",
params: { professional_network_profile_url: canonicalUrl, limit: 10 } }, // 1 cr/post — cap deliberately
];
if (companyId) {
calls.push({ name: "company_enrich",
params: { crustdata_company_ids: [companyId], exact_match: true,
fields: ["basic_info", "taxonomy"] } }); // 2 cr, exactly one match
}
const results = await parallelMap(calls, async (c) => ({ name: c.name, r: await callTool(c.name, c.params) }));
const postsR = results.find(x => x.name === "social_post_list_live")?.r;
const companyR = results.find(x => x.name === "company_enrich")?.r;
// Posts are optional: a failed or empty pull means neutral voice, not a failed run.
const posts = postsR && postsR.ok
? (postsR.data.posts ?? []).map(p => ({
text: p.text,
date: p.date_posted,
reactions: p.engagement?.total_reactions,
comments: p.engagement?.total_comments,
}))
: [];
const company = companyR && companyR.ok
? pick(companyR.data[0]?.matches?.[0]?.company_data ?? {}, ["basic_info", "taxonomy"])
: null;
// Return the smallest projection — only what the script returns reaches the model.
return {
identity: {
name: person.basic_profile?.name,
title: person.basic_profile?.current_title,
location: person.basic_profile?.location,
company: current.name,
company_domain: current.company_website_domain,
start_date: current.start_date, // tenure = today minus this
},
past_roles: (person.experience?.employment_details?.past ?? []).slice(0, 5)
.map(e => ({ company: e.name, title: e.title })),
company,
posts,
};
此脚本的注意事项:
- 绝不要在
person_enrich上设置preview: true。 该参数取决于套餐,某些账户会返回 400 错误。流程绝不能依赖它;基本费用本来就只需 1 个额度。 - 将
person_enrich的字段限制为basic_profile+experience+social_handles。 如果白名单中没有social_handles,profileUrl访问器会读取到undefined,帖子获取就会退回到用户原样输入的网址。有些字段组(certifications、honors、updated_at)受套餐限制——投影包含受限字段时,整个调用都会失败,并返回指明该字段的 403 错误。如果发生这种情况,请移除该字段并重新运行。 - 响应路径与筛选路径不同:职位名称位于
basic_profile.current_title,当前雇主位于experience.employment_details.current[].name,规范的个人资料网址位于social_handles.professional_network_identifier.profile_url(profileUrl访问器会为你读取该值)。
公司信息回退:档案中没有 company id
如果当前任职信息没有 company id,请先通过域名(或名称)解析公司。company_identify 免费且支持模糊匹配——一个标识符可能返回多家公司——因此请选择 confidence_score 最高的匹配项,然后使用 exact_match: true 按 id 补充信息。这是成本最低的精确路径:免费识别 + 恰好一条补充信息匹配结果需要 2 个积分。
// model query: resolve and enrich the user's current company by domain
const idr = await callTool("company_identify", { domains: ["example.com"] }); // ONE identifier type per call
if (!idr.ok) return { error: idr.message };
const matches = idr.data[0]?.matches ?? [];
const top = matches.slice().sort((a, b) => (b.confidence_score ?? 0) - (a.confidence_score ?? 0))[0];
if (!top) return { error: "no_company_match" };
const id = top.company_data?.basic_info?.crustdata_company_id ?? top.company_data?.crustdata_company_id;
const er = await callTool("company_enrich", {
crustdata_company_ids: [id],
exact_match: true,
fields: ["basic_info", "taxonomy"],
});
if (!er.ok) return { error: er.message };
return pick(er.data[0]?.matches?.[0]?.company_data ?? {}, ["basic_info", "taxonomy"]);
请勿在 company_identify 上投影 social_profiles——该字段受套餐限制,会导致整个调用返回 403。
根据返回的数据推导人物画像
- 身份:姓名、职位、公司、任职时长(根据
start_date),以及基于过往职位的一句话背景介绍。 - 公司与我们销售的产品:从
basic_info+taxonomy获取产品和类别;从公司简介和用户的帖子主题中提取要监控的关键词。 - 表达风格:根据实际帖子记录语气和风格——句子长度、使用第一人称还是第三人称、术语使用程度、表情符号用法、开篇方式。如果帖子为空,写“中性”即可。
- 关注的主题:帖子中反复出现的主题,并根据互动情况加权。
推断的 ICP——标注为推断结果,并确保可直接用于筛选
根据公司的销售内容和典型买家推导 ICP:行业、员工人数范围、地理位置、融资阶段、买家职位、买家资历。始终标注为 inferred——这是供用户更正的假设,而非事实。
编写可供下游搜索直接使用的 ICP 值。分类字段为封闭集合——看似合理但实际错误的值会悄然返回零条记录,因此请先通过自动补全解析这些字段(免费),再写入配置:
// model query: resolve filter-ready values for the inferred ICP
const probes = [
{ tool: "company_autocomplete", params: { field: "basic_info.industries", query: "software" } },
{ tool: "person_autocomplete", params: { field: "experience.employment_details.current.seniority_level", query: "vice" } },
];
return await parallelMap(probes, async (p) => {
const r = await callTool(p.tool, p.params);
// Returns shape is { suggestions: [{ value }] } — project to the value strings.
return { field: p.params.field, values: r.ok ? (r.data.suggestions ?? []).map(s => s.value) : [], error: r.ok ? null : r.message };
});
买家资历必须使用 experience.employment_details.current.seniority_level 中的准确词汇:Entry Level、Entry Level Manager、Experienced Manager、Senior、Director、Vice President、CXO、Owner / Partner、In Training、Strategic。不确定时,请通过 person_autocomplete 解析,而不要猜测。
准确性不容妥协
此档案会影响所有下游技能;错误信息会污染一切。
- 只填写源数据支持的信息。无法确认的内容应如实说明,不要猜测。
- 为所有推断内容添加标注(ICP 始终标注为
inferred)。 - 写入文件前先向用户展示人物画像:“以下是我对你的理解——如果有偏差,请告诉我。”根据用户的更正进行调整,然后再写入。
第 3 步:编写配置文件
在工作目录中编写两个文件。config/persona-profile.md 包含完整的人物画像;config/gtm-config.md 重复列出公司 / ICP / 表达风格的关键信息,并附上技术栈,方便每项技能一次读取即可获取这些信息。
config/persona-profile.md
# Persona Profile
Built by icp-builder on <YYYY-MM-DD>. Read by sales-prospecting, account-research, sales-outreach, and meeting-prep.
## Identity
- Name:
- Title:
- Company: <name> (<domain>)
- Tenure: since <start date>
- Background: <one line from past roles>
## Company & what we sell
- Product:
- Category:
- Keywords to monitor:
## Inferred ICP
Label: inferred from <what the company sells + typical buyers>. User-confirmed: <yes/no>
- Industries: <filter-ready values>
- Headcount:
- Geography:
- Funding stage:
- Buyer titles:
- Buyer seniority: <exact seniority vocabulary values>
## Voice
- Tone:
- Style notes:
- Always: no em dashes; never "delve", "leverage", or "streamline"; no filler;
write like a colleague.
## Topics they care about
- <from posts, weighted by engagement>
config/gtm-config.md
# GTM Config
Read by sales-prospecting, account-research, sales-outreach, and meeting-prep at startup.
## Stack
- Data provider: crustdata | none
- CRM: <tool> | none
- Calendar: <tool> | none
- Email: <tool> | none
- Call recorder: <tool> | none
- Sequencer: <tool> | none
- Team chat: <tool> | none
`none` = that slot runs draft-only: drafts and CSV exports instead of pushing to the tool.
## What we sell
<one or two lines>
## ICP (inferred)
- Industries:
- Headcount:
- Geography:
- Funding stage:
- Buyer titles:
- Buyer seniority:
## Customers
none yet — add names or domains as you close; sales-prospecting uses them for lookalikes.
## Voice
<tone in one line>. No em dashes; never "delve", "leverage", or "streamline"; no filler;
write like a colleague.
交接
总结:已连接与跳过的技术栈、用 2-3 行概述人物画像,以及哪些内容标注为推断结果。然后:
一切就绪。试试 sales-prospecting(“根据我的 ICP 建一份名单”)或 account-research(“研究 ”)——两者都会自动读取此配置。
无数据回退
如果尚未连接 Crustdata,或者补充信息结果过少(没有匹配项、档案信息稀少、帖子数量为零):
- 改为请用户用 2-3 行介绍:姓名和职位、公司业务、目标客户。这就是全部访谈内容——绝不要进行冗长问卷。
- 根据这些内容编写两个配置文件。表达风格 = 中性,并遵循 no-slop 规则。
ICP = 仍需标注为
inferred。 - 如果补充信息不完整,保留已验证的信息,明确说明哪些内容无法推断,并让用户只针对这些内容补充一行说明。
成本
person_enrich使用basic_profile+experience+social_handles:1 个积分。company_identify、company_autocomplete、person_autocomplete:免费。- 使用 id 调用
company_enrich并设置exact_match: true:匹配一条需 2 个积分。 social_post_list_live:每条帖子 1 个积分——务必有意设置limit(用于了解风格时,10 条绰绰有余)。- 一次典型的完整运行约需 13 个积分。每个
execute响应都包含credits和credits_remaining;account_credits(免费)会报告余额。
错误处理
- 在每个脚本中都根据
r.ok分支处理。 调用失败不会中止脚本;未检查的失败会悄然以空数据继续执行,看起来像是“没有结果”。 person_enrich未返回匹配结果 → 向用户确认 URL(是否有拼写错误或个性化标识符已更改),然后使用无数据回退方案。- 403 错误中提及某个字段,表示该字段的投影受套餐限制——移除该字段并重新运行。
- 帖子调用失败或返回空结果不算错误:风格将采用中性表达。
- 公司信息增强失败 → 仅根据个人数据保留人物设定,并说明缺少哪些信息。
规则
- 先表示欢迎;可选地询问一个有关技术栈的问题;URL 就是完整的访谈内容。
- 绝不根据已连接的连接器推测技术栈。 要么询问,要么写
none。 - 绝不询问他们卖什么、理想客户画像(ICP)是什么或品牌语气如何——自行推断。如果补充信息太少,就用 2-3 行概括,绝不要进行完整访谈。
- 先向对方复述用户画像,让其确认或修正,然后再写入文件。
- 标注推断内容。 对跳过的工具写
none。绝不编造技术栈或用户画像细节。 - 语气始终遵循“不写废话”原则:不用 em dash;绝不使用 “delve”、“leverage” 或 “streamline”;不写填充内容;像同事一样写作。
- 缺少配置绝不会阻塞任何操作:此技能会创建配置,后续技能在配置缺失时会指回此处。
- 根据内容调整布局,绝不能让布局隐藏任何信息。 品牌系统固定不变,布局则不固定。如果真实内容放不下——例如公司或人名很长、标题有 12 个词、表格有 200 行——就调整布局,而不是修改内容:让卡片扩展、换行而非截断、改为单列、加宽列、提高上限,或为宽内容设置独立的滚动容器。绝不要通过裁剪卡片、用省略号截断名称或悄悄丢弃行来解决空间不足的问题。如果确实必须设置上限,就在 UI 中说明(“正在显示 214 项中的前 50 项”),让读者知道还有哪些内容未显示。检查渲染结果,并在交付前修复所有被截断的内容。
- 渲染输出中的图标:使用 Lucide(仪表板的图标集),以内联 SVG 形式呈现,并使用
currentColor描边。制品 UI 中不使用表情符号。 - 用户画像本人的照片也可免费使用——
basic_profile.profile_picture_permalink包含在 Step 2 的person_enrich已返回的basic_profile组中。用户画像单页介绍的是一个人;应以内联 base64 的方式嵌入照片(遵循相同的binary/octet-stream规则),并提供姓名首字母缩写作为后备显示。 - 公司徽标可免费使用——在渲染的用户画像页面中使用。
basic_info.logo_permalink来自免费的company_identify,也来自你已为用户画像运行的company_enrich。应将其以内联 base64 的方式嵌入为data:image/jpeg;base64,...URI(媒体 CDN 会以binary/octet-stream提供这些图片,因此远程<img src>会显示空白);没有徽标时则显示姓名首字母缩写。 - 制品品牌标识:配置文件保持为纯 Markdown,不要在机器读取的文件中添加品牌装饰。但如果将用户画像渲染为页面或文档(例如用户画像单页介绍、ICP 总结文档),则应在页眉或页脚中加入 Crustdata 品牌组合标识:小号全大写 “Powered by” 眉题,以及指向 crustdata.com 的官方 Crustdata 字标。此技能的
assets/中附带成对的字标文件——crustdata-logo-light.png(深色文字,适用于浅色背景)和crustdata-logo-dark.png(白色文字,适用于深色背景);这与 app.crustdata.com 页眉渲染的文件相同。根据主题选择合适的版本,以约 17px 的高度以内联 base64 方式嵌入,绝不使用外链;渲染的制品无法获取远程图片。品牌强调色:#5547E2(产品主色;深色背景使用#8387FF)。正文字体:可嵌入时使用 Geist,否则使用系统字体栈。绝不要仅为展示品牌标识而渲染制品。
工具依赖项
此技能需要:
- Crustdata MCP 服务(install.crustdata.com/mcp):
一个提供
list_tools、get_schema和execute的 Code Mode MCP。所有 Crustdata 数据工具都通过await callTool(name, params)在execute({ code })纯 JavaScript 脚本中调用——请根据get_schema提供的类型化接口编写代码,但脚本主体中不得包含类型注解(类型注解会导致解析错误,使整个运行失败)。此处使用的工具:person_enrich、company_identify、company_enrich、social_post_list_live、company_autocomplete、person_autocomplete、account_credits。 - 对工作目录的写入权限——用于创建
config/persona-profile.md和config/gtm-config.md。
此技能与 sales-prospecting 和 account-research 一同提供;它们会读取此技能写入的配置。