Skip to content

Roadmate Concept Product: Three-Stage Pipeline for Social Interest Tags

About 1005 wordsAbout 3 min

2026-07-30

GitHub repo

roadmatePublic

👭 RoadMate — light social for strangers (AI hardware)

TypeScript

0

0

The problem to solve

The product does not need vague categories like “music / travel.” It needs concrete topics you can open with in person.

Examples: “2026 World Cup,” “Xiamen National Day free trip,” “pour-over coffee enthusiasts,” and so on.

It also needs to answer:

  1. How often does this interest appear?
  2. How fresh is it?
  3. Which posts support it?

Without an attribution chain, recency weighting is hard, and so is evaluating whether an inference is trustworthy.

Why naive approaches fall short

We tried three approaches in sequence.

Approach A: Per-post parallel extraction

Good throughput; per-post attribution is clear. But each post emits its own tags — near-duplicates are hard to merge, and there is no global view.

Approach B: Rolling corpus compression

You can carry a prior forward and save context. After batch merges it is hard to stably return to a single post; freshness weights become unreliable; intermediate steps are hard to assert.

What we actually need:

Keep post-level time, do global semantic dedup, and still trace final tags back to source posts.

Core design

Current approach C splits the work into three stages, then lets code compute weights:

Post input
  → Stage 1 parallel preprocess
  → Stage 2 timeline merge
  → Stage 3 tag extraction
  → Code aggregates frequency / sentiment / recency / weight
  → Embedding
  → Word cloud / device matching
StageWhat the model doesWhat code does
1 PreprocessDetect spam posts; compress to short summariesConcurrent scheduling; filter noise
2 Timeline mergeMerge semantically similar posts within ~7 daysMerge time = latest post
3 Tag extractionEmit icebreaker tags, sentiment, source entriesFrequency, recency, weight, eviction

Product icebreaker rules live mainly in stage 3. The first two stages are engineering preprocess — tunable in isolation without cascading breakage.

Approach A/B code remains for comparison, but the Web UI and bench:timeline both run approach C.

Key mechanisms

1. Attribution chain

Tags are not done once bound to post IDs. The chain is:

Tag entryIds → timeline entry sourcePostIds → post createdAt

So frequency and recency are computed in code from real timestamps — not verbal freshness guesses from the model.

2. Timeline merge window

Content that is highly similar within an adjacent 7-day window can merge, to control context length.

Same-theme posts more than 7 days apart do not merge. Frequency can still reflect cross-period repeat interest — e.g. coffee mentioned again weeks later.

If the model fails to merge, fall back to “one post, one entry.” Posts are not dropped; dedup is just weaker.

3. Weight formula

Same-name tags merge case-insensitively first, then three dimensions:

  • frequency: expanded source-post count / total posts
  • sentiment: mean sentiment across source entries
  • recency: based on last occurrence, exp(-λ × days since), λ = 0.08

Final:

weight = 0.40 × frequency + 0.20 × sentiment × recency + 0.40 × recency

Sentiment is multiplied by recency so older interests’ sentiment contribution also decays.

Filters:

  • Keep only tags that appear in at least 1 post
  • Appear once and older than 60 days → drop
  • Take top 20 by weight

Coefficients and windows live in constants.ts.

4. Full re-run

Every “infer and save” re-runs all three stages — no incremental skip.

That buys reproducibility and avoids rolling-prior drift. Cost: higher latency on long lists.

5. Embedding and word cloud

Vectors are built only for aggregated tag names. New tags are generated lazily.

Ball size in the word cloud is relative rank after min-max normalization within the current batch — not a linear map of absolute weight to pixels. Custom tags map from slider weight absolutely.

Execution flow

flowchart LR
  P[Post list / X fetch] --> S1[Stage 1 preprocess]
  S1 --> S2[Stage 2 merge]
  S2 --> S3[Stage 3 extract]
  S3 --> A[Code aggregate]
  A --> E[Embedding]
  E --> U[Word cloud / match score]

Two input modes:

  • Post list: paste, or import/export as roadmate-posts/1 text
  • X username: pull original tweets via twitterapi.io into the same post structure

Post lists are not written to localStorage. After refresh, re-import or re-fetch. Profiles store only tags and embeddings.

Deliberately not doing

  • Do not treat A/B as the main path — comparison only.
  • Do not let the model emit final weight. Frequency and recency are code’s job.
  • No incremental inference yet — reproducibility and evaluability first.
  • Do not persist raw post text into the browser profile.

Relation to other modules

After inference is written to the local profile, Playground scores match via embedding cosine and tag overlap.

The device side does not care about the three stages — only final tag vectors. The split lets “who is worth approaching” and “how to respond when close” iterate separately.

Evaluation

CLI and Web UI share one pipeline:

npm run bench:timeline
npm run bench:timeline -- --verbose
npm run bench:timeline -- --case multi-theme-user

Cases live in scripts/fixtures/corpus-cases/. Assertions can check keyword hits, banned words, tag counts, and a floor on valid posts.

--verbose prints per-post noise judgments, merge entries, and the final weight table — useful for locating which stage failed.

Tuning knobs

ConstantRole
WEIGHT_FACTORSThree-way weight mix
RECENCY_DECAY_LAMBDAHow steep time decay is
TIMELINE_MERGE_WINDOW_DAYSMerge window
MAX_INFERRED_TAGS / STALE_TAG_DAYS / LLM_CONCURRENCYOutput cap, stale eviction, concurrency

Orchestration and prompts mainly live in:

  • server/timelineInference.ts
  • prompts.ts
  • tagUtils.ts
  • api/openrouter.ts

Summary

The core of this inference design:

Preserve post-level time attribution first, then global semantic dedup, then reproducible interest weights in code.

Concretely:

  • Stage 1: throughput and noise detection
  • Stage 2: control duplicates and context length
  • Stage 3: icebreaker-ready tags
  • Code owns frequency / recency / weight and feeds embeddings

Result: tags that are more concrete, explainable, and evaluable — and that stably drive near-field matching.

Changelog

8/27/26, 2:39 PM
View All Changelog
  • 68955-Translate all 14 blog posts to English for Plume i18n.on
  • 4b31c-优化关于页展示,并清理失效文档链接。on
  • b7b2c-启用文章变更历史并升级主题配置。on
  • 4e4c9-更新博客内容与开发体验:置顶两篇工程化文章,Roadmate 加入演示视频,docs:dev 有缓存时跳过 RepoCard 拉取on
  • 3222f-Roadmate 标题改为:三阶段流水线提取社媒兴趣标签on
  • 0fe83-Roadmate 标题改为:三阶段分析社交媒体兴趣标签on
  • 758fd-调整三篇博客排序与元数据:去 tag、声明式置顶、Roadmate 标题突出三阶段on
  • 029e1-删除两篇博客:Roadmate 近场配对与 BabyLovable WorkflowAgenton
  • 142a9-优化博客标题与 Roadmate 阅读体验on