跳到正文
DEV Community· Samina Parveen·· 3 小時前AI 評分50

WhyBase 用知識圖譜識別決策變更後過時的企業文件

Your company's docs are lying to you. A graph can tell you which ones.

AI 導讀

Samina Parveen 等人構建 WhyBase,以知識圖譜追蹤企業決策及文件依賴,找出決策被取代後過時的文件。它以 ASSUMES 關係連結文件與決策,沿 SUPERSEDES 關係識別失效文件,並按作者是否仍在職安排修訂人選。系統以 7 個工具對應手寫 Cypher 查詢,LLM 負責選擇工具;讀取操作唯讀,寫入須經確認,示範資料均為合成資料。

正文

 # Your company's docs are lying to you. A graph can tell you which ones.

At most companies, "why do we use Kafka?" has an answer. It's in ADR-007, written two years ago by someone who has since left. And the onboarding guide, the ledger runbook and the event-schema doc were all written assuming Kafka. If the team switches to Redpanda next week, all three are wrong the moment the decision is made, and nothing tells you so.

We built WhyBase for Graph Hacks (WeMakeDevs × FalkorDB, Track 03: Company Brain) to fix that second problem. Search tools already help with the first one. WhyBase knows why a decision was made, and it knows when a doc has gone out of date.

Search can find the doc. It can't tell you the doc is wrong.

The obvious way to build a "company brain" is to embed every document and do retrieval. That finds the doc that mentions Kafka, but "this doc is now wrong" isn't a property of the text. It depends on facts outside the doc:

  • the doc assumed decision D-002 ("use Kafka for the event bus"),
  • D-002 was superseded yesterday by D-022 ("move to Redpanda"),
  • the doc's author left in July,
  • two people on the same team still write about that system.

Those are relationships, so we stored them as a graph.

The model

Six node labels and twelve relationship types:

Person -MEMBER_OF-> Team -OWNS-> System
Project -USES-> System
Project -RELIES_ON-> Decision
Decision -RELIES_ON-> Decision
Decision -SUPERSEDES-> Decision
Decision -ABOUT-> System
Decision -RECORDED_IN-> Source
Decision -MADE_BY-> Person
Source -AUTHORED_BY-> Person
Source -MENTIONS-> System
Source -ASSUMES-> Decision

ASSUMES does the most work. It means "this doc is only true while that decision holds." A doc that ASSUMES a superseded decision is stale by definition, with no NLP involved.

Our demo company is Kettle Labs, which is fictional: 23 people, 6 teams, 13 systems, 10 projects, 47 sources and 21 decisions, all synthetic. We wrote the data so the interesting cases show up: superseded decision chains, people who left, and a system nobody has written about.

Every question is a traversal

"Why do we use Kafka?" Start at the system, find the active decision about it, then walk the supersede chain backwards with its sources and deciders:

MATCH (:System {name:$system})<-[:ABOUT]-(d:Decision {status:'active'})
MATCH p = (d)-[:SUPERSEDES*0..5]->(old:Decision)
MATCH (old)-[:RECORDED_IN]->(src:Source)
OPTIONAL MATCH (old)-[:MADE_BY]->(who:Person)
RETURN d.id AS head, length(p) AS step, old.id AS id, old.summary AS decision,
       src.title AS source, who.name AS by
ORDER BY head, step

The answer comes back as a path, event-bus ← D-002 → SUPERSEDES → D-001, with ADR-007 and ADR-001 attached. The UI draws that path on the graph, so you can check the answer yourself.

"Which docs are now wrong, and who should fix them?" This one query is why we used a graph:

MATCH (src:Source)-[:ASSUMES]->(old:Decision {status:'superseded'})
MATCH (old)<-[:SUPERSEDES*1..5]-(head:Decision {status:'active'})
OPTIONAL MATCH (src)-[:AUTHORED_BY]->(author:Person)
OPTIONAL MATCH (author)-[:MEMBER_OF]->(:Team)<-[:MEMBER_OF]-(mate:Person {status:'active'})
...
RETURN src.title AS stale_doc, head.summary AS now_true,
       CASE WHEN author.status = 'active' THEN [author.name] ELSE mates[..2] END AS fix_by

If the author is still around, they get the fix. If they've left, it goes to active teammates, ranked by how many docs they've written about the same systems.

"What breaks if we reverse D-002?" is a variable-length walk over RELIES_ON, up to three hops, across decisions and projects:

MATCH p = (x)-[:RELIES_ON*1..3]->(:Decision {id:$id})
RETURN DISTINCT coalesce(x.name, x.id) AS affected, length(p) AS hops
ORDER BY hops

On our data, reversing D-002 hits 7 things, including the Instant Refunds project, three hops away.

"Which decisions are load-bearing?" FalkorDB has PageRank built in, so this took one line:

CALL algo.pageRank('Decision', 'RELIES_ON') YIELD node, score

D-002 ranks first. That's useful to know before anyone reverses it.

We also answer "what did we believe about event-bus on 2025-01-01?" by filtering decisions on at <= date < superseded_at (it was RabbitMQ). "Which systems does only one active person still know?" comes from counting authors per system, split by active and left. payments-ledger shows up: Arjun wrote five docs about it and left, and Rohan is the only active person who knows it.

The demo moment: a decision changes

You paste a message into the app:

Team call today: we're moving event-bus from Kafka to Redpanda. Kafka ops cost is too high and Redpanda speaks the Kafka API. — Priya Nair

WhyBase reads it and proposes a write: system event-bus, replaces D-002, decided by Priya Nair. Nothing is written until you click Confirm & record. Then it creates D-022 with a SUPERSEDES edge to D-002, and the stale-docs counter jumps from 3 to 6. Three docs flash orange. The ledger guide's author has left, so its fix goes to Rohan Das and Ishaan Kapoor.

Ask "Why do we use Kafka?" again and you no longer get a plain answer. It comes back marked out of date, pointing to D-022.

Keeping the AI honest

An LLM that writes its own Cypher against your company graph is a bad idea, so ours doesn't.

  • The model only picks tools. There are seven (why, stale_docs, who_knows, knowledge_risk, reversal_impact, load_bearing, as_of), and each maps to one hand-written .cypher file. The model fills in arguments and never sees a query string. There are 30 query files in total, all in backend/queries/, all parameterised.
  • Names are resolved first. Every system, person or decision the model mentions is looked up in the graph before any traversal runs. Ask "Who knows quantum-cache?" and the answer is "unknown", with only the resolver in the trace.
  • Reads can't write. Every agent read goes through graph.ro_query. Only the confirmed ingest step uses graph.query.
  • The answer gets checked. Every name or ID in the model's sentence has to appear in the rows the query returned. If one doesn't, we throw the sentence away and use a template built from the rows. The footer says which name failed.

The app also runs with no LLM at all. A keyword router picks the tool and the answers are template sentences. That turned out to matter, because the Gemini free tier we tested on allows only 20 requests a day for some models. We defaulted to gemini-3.1-flash-lite, and when the quota runs out the header says so and answers come from the offline router.

Things that bit us

The client connects in its constructor. FalkorDB(host=...) runs INFO straight away to check for Sentinel. On our first Render deploy the host was wrong, and the app died on startup with a raw redis ConnectionError instead of showing our "Graph down" banner. The fix was to treat building the client like any other query that can fail.

"localhost" means something different in the cloud. Our second deploy failed the same way, because the Render environment had FALKOR_HOST=localhost. On Render that's the app's own container, and no database was running there.

So we put the database in the container. The final image copies redis-server and falkordb.so from the official FalkorDB image (pinned by digest) into our Python image. A small start script launches it when no external host is set. The whole thing runs in under 190 MB on Render's free 512 MB tier. The graph is in-memory and the app reloads Kettle Labs on every start. That's fine for a demo, and the same image still talks to an external FalkorDB when you set FALKOR_HOST.

Smaller ones. Reading from a graph that doesn't exist yet raises an "empty key" error instead of returning no rows, so our auto-seed had to handle that. The Docker image's default 1-second query timeout occasionally tripped on reads, so read queries retry once.

What we'd build next

  • Ingest from Slack and GitHub directly, instead of pasting messages
  • Infer ASSUMES edges when a doc is written, instead of hand-labelling them in the seed data
  • Send "your doc is out of date" notifications to the people fix_by names

Try it

Open https://whybase.onrender.com. It's a free instance, so the first load can take up to 50 seconds. Use the Examples button for ready-made questions, and Reset demo puts the graph back. Every query the product uses is listed in the README, and docker compose up runs it locally.

Built by Samina Parveen, Madhur Tandon and Aditya Karn. We wrote the spec, and Claude Code generated most of the code from it; details are in AI_DISCLOSURE.md. All data is synthetic.

來源:DEV Community · dev.to