客户所在之处
给定产品功能和理想客户画像(ICP),追踪真实的 ICP 痛点帖文回到其来源社区。叠加竞品讨论信号。通过 DuckDuckGo 发现 Slack/Discord/新闻通讯/播客/会议渠道。按 ICP 信号数量、规模、活跃度和竞品存在度为每个渠道打分。输出一个排序的作战手册:每个渠道包含证据、进入战术、内容策略和反模式。不做猜测。仅限信号追踪到的渠道。
关键规则: 输出中的每个渠道名称必须存在于本次运行的 Reddit API 响应或 DuckDuckGo 搜索结果中。每个成员数必须来自 about.json API 或搜索摘要——绝对不能估算。每个 ICP 信号数必须与原始数据一致。如果某渠道类型结果为 0,则报告 0——不要编造渠道。
常见错误
| Agent 会倾向于…… | 为什么这是错的 |
|---|---|
| 推荐通用渠道("LinkedIn", "Twitter") | 每个渠道必须具体,包含名称、成员数和 URL。"LinkedIn 群组:DevOps for Enterprise Teams (45K 成员)"——而不是仅仅 "LinkedIn"。 |
| 对每个 ICP 使用相同的渠道 | 信号追踪是 ICP 特定的。DevOps ICP 和金融 ICP 会产生完全不同的渠道列表。每次 ICP 都要重新运行脚本。 |
| 编造成员数或社区名称 | 每个渠道名称必须来自 DuckDuckGo 结果或 Reddit API。每个成员数必须来自 API 或搜索摘要。如果不可用,写上"成员数未找到"。 |
| 跳过竞品层 | 竞品被讨论的地方 = 你的 ICP 正在评估替代方案 = 最热的外展上下文。即使用户没要求,也要始终运行竞品搜索。 |
| 把产品推销写成进入战术 | "在 r/devops 发帖介绍你的产品"不是进入战术。进入战术要指明具体的帖子类型、内容格式和社区规范。 |
| 把 Reddit 当作唯一的渠道类型 | 输出必须包含至少 3 种渠道类型。如果只找到 Reddit,在停止前明确使用 DuckDuckGo 搜索 Slack/Discord/新闻通讯/会议。 |
步骤 1:设置检查
echo "GITHUB_TOKEN: ${GITHUB_TOKEN:-未设置 -- 竞品层以 60 请求/小时未认证速率运行}"
echo ""
echo "本次运行将使用的数据源:"
echo " Reddit public JSON (无需认证, 信号追踪)"
echo " Reddit about.json (无需认证, subreddit 元数据)"
echo " HN Algolia API (无需认证, 信号追踪)"
echo " DuckDuckGo HTML (无需认证, 渠道发现)"
echo " GitHub API (${GITHUB_TOKEN:+已认证, }可选用于竞品丰富)"
如果 GITHUB_TOKEN 未设置:继续。所有核心渠道发现无需它即可工作。
步骤 2:解析 ICP
从对话中收集:
product-- 产品功能(一句话)icp_role-- ICP 的角色(例如 "技术联合创始人", "A 轮公司的 DevOps 工程师")icp_pain-- ICP 的主要问题(例如 "客户获取", "警报疲劳")category-- 市场类别关键词(例如 "初创企业 GTM 销售", "devops 监控")competitors-- 可选的竞品名称(例如 "Clay, Apollo, HubSpot")
ICP 级联:
- 如果用户的提示包含 product + icp_role + icp_pain:直接提取并继续。
- 如果提示信息不足(仅类别或仅产品名称):检查
docs/icp.md是否有保存的 ICP 配置文件。与提示中的详细信息合并。 - 如果仍然不足(缺少 icp_role 或 icp_pain):一次一个问题,提出以下 3 个问题:
- "用一句话描述你的产品做什么?"
- "你的理想客户是谁?(角色、公司类型、团队规模)"
- "他们在找到你的产品前的主要问题是什么?"
- 将最终的 ICP 保存到
docs/icp.md,以便其他技能可以重用。
如果 docs/icp.md 尚未包含此产品,则保存 ICP 文件:
python3 << 'PYEOF'
import json, os
icp = {
"product": "PRODUCT_HERE",
"icp_role": "ICP_ROLE_HERE",
"icp_pain": "ICP_PAIN_HERE",
"competitors": ["COMP_1", "COMP_2"],
"category": "CATEGORY_HERE"
}
os.makedirs("docs", exist_ok=True)
with open("/tmp/wcl-input.json", "w") as f:
json.dump(icp, f, indent=2)
# 更新 docs/icp.md
icp_md_path = "docs/icp.md"
new_block = f"""## {icp['product']}
- **ICP 角色:** {icp['icp_role']}
- **ICP 痛点:** {icp['icp_pain']}
- **竞品:** {', '.join(icp['competitors']) if icp['competitors'] else '无'}
- **类别:** {icp['category']}
"""
existing = open(icp_md_path).read() if os.path.exists(icp_md_path) else ""
if icp['product'] not in existing:
with open(icp_md_path, "a") as f:
f.write(new_block)
print(f"ICP 已保存到 {icp_md_path}")
else:
print(f"ICP 已存在于 {icp_md_path}")
print(f"产品:{icp['product']}")
print(f"ICP 角色:{icp['icp_role']}")
print(f"ICP 痛点:{icp['icp_pain']}")
print(f"竞品:{', '.join(icp['competitors']) if icp['competitors'] else '无'}")
PYEOF
步骤 3:运行独立数据收集脚本
检查脚本是否存在:
ls scripts/fetch.py 2>/dev/null && echo "脚本可用" || echo "未找到"
运行渠道发现:
GITHUB_TOKEN="${GITHUB_TOKEN:-}" python3 scripts/fetch.py \
"$(python3 -c "import json; d=json.load(open('/tmp/wcl-input.json')); print(d['category'])")" \
--icp-role "$(python3 -c "import json; d=json.load(open('/tmp/wcl-input.json')); print(d['icp_role'])")" \
--icp-pain "$(python3 -c "import json; d=json.load(open('/tmp/wcl-input.json')); print(d['icp_pain'])")" \
--product "$(python3 -c "import json; d=json.load(open('/tmp/wcl-input.json')); print(d['product'])")" \
--competitors "$(python3 -c "import json; d=json.load(open('/tmp/wcl-input.json')); print(','.join(d['competitors']))")" \
--output /tmp/wcl-raw.json
等待完成(最多允许 5 分钟——Reddit + DuckDuckGo 搜索大约需要 120 秒)。
验证输出:
python3 -c "
import json
with open('/tmp/wcl-raw.json') as f:
d = json.load(f)
print(f'Reddit 帖子: {d[\"reddit_posts_found\"]}')
print(f'HN 信号: {d[\"hn_signals_found\"]}')
print(f'发现的渠道: {d[\"summary\"][\"total_channels\"]}')
print(f'最高优先级: {len(d[\"summary\"][\"top_priority\"])}')
print(f'按类型分类: {d[\"summary\"][\"by_type\"]}')
print(f'竞品层已运行: {d[\"summary\"][\"competitor_layer_ran\"]}')
"">
如果 total_channels < 3:告诉用户:"找到的渠道少于 3 个。ICP 描述可能对于 Reddit/DDG 覆盖范围来说过于狭窄。请尝试更广泛的类别关键词,或添加竞品名称以激活竞品层。"然后在停止前,用更广泛的类别关键词尝试一次重试。
步骤 4:打印渠道摘要
加载原始数据并打印排序的摘要表格:
python3 -c "
import json
with open('/tmp/wcl-raw.json') as f:
d = json.load(f)
channels = d['channels_discovered']
print(f'渠道数: {len(channels)}')
print()
print(f'{'#':<4} {'渠道':<35} {'类型':<14} {'成员':<12} {'ICP 信号':<13} {'得分':<8} 层级')
print('-' * 100)
for i, ch in enumerate(channels[:15], 1):
members = ch.get('members', 0)
m_str = f'{members//1000}K' if members >= 1000 else str(members) if members else '?'
print(f'{i:<4} {ch[\"name\"]:<35} {ch[\"type\"]:<14} {m_str:<12} {ch.get(\"icp_signal_count\",0):<13} {ch.get(\"channel_score\",0):<8} {ch.get(\"tier\",\"\")}')
"">
从得分最高的渠道打印前 3 个证据帖子:
python3 -c "
import json
with open('/tmp/wcl-raw.json') as f:
d = json.load(f)
channels = d['channels_discovered']
if channels:
top = channels[0]
print(f'最高渠道: {top[\"name\"]}')
print(f'证据帖子:')
for ep in top.get('evidence_posts', [])[:3]:
print(f' [{ep.get(\"score\",0):.0f}] {ep.get(\"title\",\"\")}')
print(f' {ep.get(\"url\",\"\")}')
"">
步骤 5:AI 渠道丰富
你现在有了原始渠道数据。对于 top-priority 和 high 层级中的每个渠道,生成一个作战手册条目。
加载所有渠道:
python3 -c "
import json
with open('/tmp/wcl-raw.json') as f:
d = json.load(f)
top_channels = [ch for ch in d['channels_discovered'] if ch.get('tier') in ('top-priority', 'high')]
print(json.dumps(top_channels, indent=2))
"">
对于上面的每个渠道,生成:
who_is_here: 2 句话描述该渠道中 ICP 的具体类型。从证据帖子、subreddit 描述和 ICP 档案中推导。不要写"你的目标受众"——要具体。例如:"50-500 人公司中拥有基础设施堆栈但没有专职 SRE 团队的 DevOps 工程师。他们发布关于值班倦怠、Kubernetes 蔓延以及选择云原生还是自托管可观测性的帖子。"
entry_tactic: 一个具体、可操作的进入策略。指明帖子类型、发布格式和社区规范。不要写"参与社区"。例如:"找到每周的'你在做什么?'帖子(由 automoderator 每周一发布)。回复 3 句话你解决的技术挑战——出错了什么,你尝试了什么,什么有效。不要提及产品。在发布独立内容之前建立声望。"
content_angle: 在此特定渠道中获得最高参与度的内容格式,从证据帖子的标题和得分中推导。例如:"技术事后分析比产品公告的参与度高出 5:1。格式:'我们将 200K 用户从 X 迁移到 Y——以下是出问题的地方和原因。'具体数字 + 失败的细节 = 最多点赞。"
anti_patterns: 2-3 个具体行为,可能导致帖子被删除或声誉毁于一旦。从 subreddit 规则(如果在描述中可用)和证据帖子模式中推导。例如:["在非推广帖子中发布产品链接——版主数小时内删除", "在没有具体上下文的情况下问'你们用什么工具?'——被标记为市场研究收集"]
将丰富的作战手册写入 /tmp/wcl-channels.json:
{
"playbook": [
{
"channel": "r/devops",
"evidence": "在此追踪到 34 个 ICP 信号,平均痛苦得分 180",
"who_is_here": "...",
"entry_tactic": "...",
"content_angle": "...",
"anti_patterns": ["...", "..."]
}
]
}
python3 -c "
import json
with open('/tmp/wcl-channels.json') as f:
d = json.load(f)
print(f'作战手册条目数: {len(d[\"playbook\"])}')
for p in d['playbook']:
print(f' {p[\"channel\"]}')
"">
步骤 6:生成完整排序输出
将完整的排序作战手册写入 /tmp/wcl-output.json:
python3 << 'PYEOF'
import json
from datetime import datetime
with open('/tmp/wcl-input.json') as f:
inp = json.load(f)
with open('/tmp/wcl-raw.json') as f:
raw = json.load(f)
with open('/tmp/wcl-channels.json') as f:
enriched = json.load(f)
playbook_by_channel = {p['channel']: p for p in enriched['playbook']}
channels = raw['channels_discovered']
output = {
"date": raw['date'],
"product": inp['product'],
"icp_role": inp['icp_role'],
"icp_pain": inp['icp_pain'],
"competitors": inp.get('competitors', []),
"total_channels": raw['summary']['total_channels'],
"channels": []
}
for ch in channels:
name = ch['name']
playbook = playbook_by_channel.get(name, {})
output['channels'].append({
"rank": channels.index(ch) + 1,
"name": name,
"type": ch['type'],
"url": ch['url'],
"members": ch.get('members', 0),
"active_users": ch.get('active_users', 0),
"icp_signal_count": ch.get('icp_signal_count', 0),
"competitor_mentions": ch.get('competitor_mentions', 0),
"channel_score": ch.get('channel_score', 0),
"tier": ch.get('tier', ''),
"entry_type": ch.get('entry_type', 'open'),
"evidence_posts": ch.get('evidence_posts', []),
"who_is_here": playbook.get('who_is_here', ''),
"entry_tactic": playbook.get('entry_tactic', ''),
"content_angle": playbook.get('content_angle', ''),
"anti_patterns": playbook.get('anti_patterns', []),
})
with open('/tmp/wcl-output.json', 'w') as f:
json.dump(output, f, indent=2)
print(f"输出已写入:/tmp/wcl-output.json")
print(f"渠道总数:{len(output['channels'])}")
PYEOF
步骤 7:自检 QA
python3 -c "
import json
with open('/tmp/wcl-raw.json') as f:
raw = json.load(f)
with open('/tmp/wcl-output.json') as f:
output = json.load(f)
full_text = json.dumps(output)
raw_channel_names = {ch['name'].lower() for ch in raw['channels_discovered']}
passes = 0
fails = 0
# 检查 1:没有长破折号
if chr(8212) in full_text:
print('失败:输出中发现长破折号——请替换为连字符')
fails += 1
else:
print('通过:没有长破折号')
passes += 1
# 检查 2:没有禁用词
banned = ['powerful', 'robust', 'seamless', 'innovative', 'game-changing',
'streamline', 'leverage', 'transform', 'revolutionize']
found = [w for w in banned if w.lower() in full_text.lower()]
if found:
print(f'失败:禁用词:{found}')
fails += 1
else:
print('通过:没有禁用词')
passes += 1
# 检查 3:至少 3 种渠道类型
types = {ch['type'] for ch in output['channels']}
if len(types) < 3:
print(f'失败:输出中只有 {len(types)} 渠道类型:{types}')
fails += 1
else:
print(f'通过:{len(types)} 渠道类型:{types}')
passes += 1
# 检查 4:所有渠道名称存在于原始数据中
for ch in output['channels']:
if ch['name'].lower() not in raw_channel_names:
print(f'失败:原始数据中无此渠道:{ch[\"name\"]}')
fails += 1
if fails == 0:
print('通过:所有渠道名称已验证存在于原始数据')
passes += 1
# 检查 5:没有通用进入战术
generic_phrases = ['engage with the community', 'post about your product', 'share your content']
for ch in output['channels']:
tactic = ch.get('entry_tactic', '').lower()
for phrase in generic_phrases
…
[已截断以进行翻译]